2 # Maintain "what's cooking" messages
4 my $MASTER = 'master'; # for now
8 my %reverts = ('next' => {
16 my (@u) = grep { $uniq{$_}++ == 0 } sort @_;
18 for (my $i = 0; $i < @u; $i++) {
22 } elsif ($i < @u - 2) {
29 sub describe_relation {
30 my ($topic_info) = @_;
33 if (exists $topic_info->{'used'}) {
34 push @desc, ("is used by " .
35 phrase_these(@{$topic_info->{'used'}}));
38 if (exists $topic_info->{'uses'}) {
39 push @desc, ("uses " .
40 phrase_these(@{$topic_info->{'uses'}}));
43 if (0 && exists $topic_info->{'shares'}) {
44 push @desc, ("shares commits with " .
45 phrase_these(@{$topic_info->{'shares'}}));
52 return "(this branch " . join("; ", @desc) . ".)";
56 my ($topic, $fork, $forkee, @overlap) = @_;
57 my %ovl = map { $_ => 1 } (@overlap, @{$topic->{$forkee}{'log'}});
59 push @{$topic->{$fork}{'uses'}}, $forkee;
60 push @{$topic->{$forkee}{'used'}}, $fork;
61 @{$topic->{$fork}{'log'}} = (grep { !exists $ovl{$_} }
62 @{$topic->{$fork}{'log'}});
66 my ($topic, $one, $two) = @_;
70 qw(git log --abbrev), "--format=%m %h",
71 "$one...$two", "^$MASTER")
72 or die "$!: open log --left-right";
75 my ($sign, $sha1) = /^(.) (.*)/;
78 } elsif ($sign eq '>') {
82 close($fh) or die "$!: close log --left-right";
86 forks_from($topic, $two, $one);
89 forks_from($topic, $one, $two);
91 push @{$topic->{$one}{'shares'}}, $two;
92 push @{$topic->{$two}{'shares'}}, $one;
97 Inspect the current set of topics
103 'tipdate' => date of the tip commit,
104 'desc' => description string,
105 'log' => [ $commit,... ],
112 my (@base) = ($MASTER, 'next', 'seen');
115 qw(git for-each-ref),
116 "--format=%(refname:short) %(committerdate:iso8601)",
118 or die "$!: open for-each-ref";
124 my ($branch, $date) = /^(\S+) (.*)$/;
126 next if ($branch =~ m|^../wip-|);
127 push @topic, $branch;
134 close($fh) or die "$!: close for-each-ref";
136 my %base = map { $_ => undef } @base;
138 my $show_branch_batch = 20;
141 my @t = (@base, splice(@topic, 0, $show_branch_batch));
142 my $header_delim = '-' x scalar(@t);
143 my $contain_pat = '.' x scalar(@t);
144 open($fh, '-|', qw(git show-branch --sparse --sha1-name),
145 map { "refs/heads/$_" } @t)
146 or die "$!: open show-branch";
150 if (/^$header_delim$/) {
151 $header_delim = undef;
155 my ($contain, $sha1, $log) =
156 ($_ =~ /^($contain_pat) \[([0-9a-f]+)\] (.*)$/);
158 for (my $i = 0; $i < @t; $i++) {
160 my $sign = substr($contain, $i, 1);
161 next if ($sign eq ' ');
162 next if (substr($contain, 0, 1) ne ' ');
164 if (!exists $commit{$sha1}) {
170 my $co = $commit{$sha1};
171 if (!exists $reverts{$branch}{$sha1}) {
172 $co->{'branch'}{$branch} = 1;
174 next if (exists $base{$branch});
175 push @{$topic{$branch}{'log'}}, $sha1;
178 close($fh) or die "$!: close show-branch";
182 for my $sha1 (keys %commit) {
184 my $co = $commit{$sha1};
185 if (exists $co->{'branch'}{'next'}) {
187 } elsif (exists $co->{'branch'}{'seen'}) {
192 $co->{'log'} = $sign . ' ' . $co->{'log'};
193 my @t = (sort grep { !exists $base{$_} }
194 keys %{$co->{'branch'}});
200 for my $combo (keys %shared) {
201 my @combo = split(' ', $combo);
202 for (my $i = 0; $i < @combo - 1; $i++) {
203 for (my $j = $i + 1; $j < @combo; $j++) {
204 topic_relation(\%topic, $combo[$i], $combo[$j]);
210 qw(git log --first-parent --abbrev),
211 "--format=%ci %h %p :%s", "$MASTER..next")
212 or die "$!: open log $MASTER..next";
214 my ($date, $commit, $parent, $tips);
215 unless (($date, $commit, $parent, $tips) =
216 /^([-0-9]+) ..:..:.. .\d{4} (\S+) (\S+) ([^:]*):/) {
219 for my $tip (split(' ', $tips)) {
220 my $co = $commit{$tip};
221 next unless ($co->{'branch'}{'next'});
222 $co->{'merged'} = " (merged to 'next' on $date at $commit)";
225 close($fh) or die "$!: close log $MASTER..next";
227 for my $branch (keys %topic) {
229 my $n = scalar(@{$topic{$branch}{'log'}});
231 delete $topic{$branch};
238 my $d = $topic{$branch}{'tipdate'};
239 my $head = "* $branch ($d) $n\n";
241 for (@{$topic{$branch}{'log'}}) {
242 my $co = $commit{$_};
243 if (exists $co->{'merged'}) {
244 push @desc, $co->{'merged'};
246 push @desc, $commit{$_}->{'log'};
250 @desc = @desc[0..99];
254 my $list = join("\n", map { " " . $_ } @desc);
257 # This is done a bit too early. We grabbed all
258 # under refs/heads/??/* without caring if they are
259 # merged to 'seen' yet, and it is correct because
260 # we want to describe a topic that is in the old
261 # edition that is tentatively kicked out of 'seen'.
262 # However, we do not want to say a topic is used
263 # by a new topic that is not yet in 'seen'!
264 my $relation = describe_relation($topic{$branch});
265 $topic{$branch}{'desc'} = $head . $list;
267 $topic{$branch}{'desc'} .= "\n $relation";
275 my ($mon, $year, $issue, $dow, $date,
276 $master_at, $next_at, $text) = @_;
278 my $now_string = localtime;
279 my ($current_dow, $current_mon, $current_date, $current_year) =
280 ($now_string =~ /^(\w+) (\w+) (\d+) [\d:]+ (\d+)$/);
282 $mon ||= $current_mon;
283 $year ||= $current_year;
285 $dow ||= $current_dow;
286 $date ||= $current_date;
287 $master_at ||= '0' x 40;
288 $next_at ||= '0' x 40;
290 Here are the topics that have been cooking in my tree. Commits
291 prefixed with '-' are only in 'seen' while commits prefixed with '+'
292 are in 'next'. The ones marked with '.' do not appear in any of the
293 integration branches, but I am still holding onto them. Generally,
294 being in 'next' is a sign that a topic is stable enough to be used
295 and are candidate to be in a future release, while being in 'seen'
296 means nothing more than that the maintainer has found it interesting
297 for some reason (like "it may have hard-to-resolve conflicts with
298 another topic already in flight" or "this may turn out to be
299 useful")---do not read too much into a topic being in (or not in)
303 Copies of the source code to Git live in many repositories, and the
304 following is a list of the ones I push into or their mirrors. Some
305 repositories have only a subset of branches.
307 With maint, master, next, seen, todo:
309 git://git.kernel.org/pub/scm/git/git.git/
310 git://repo.or.cz/alt-git.git/
311 https://kernel.googlesource.com/pub/scm/git/git/
312 https://github.com/git/git/
313 https://gitlab.com/git-vcs/git/
315 With all the integration branches and topics broken out:
317 https://github.com/gitster/git/
319 Even though the preformatted documentation in HTML and man format
320 are not sources, they are published in these repositories for
321 convenience (replace "htmldocs" with "manpages" for the manual
324 git://git.kernel.org/pub/scm/git/git-htmldocs.git/
325 https://github.com/gitster/git-htmldocs.git/
327 Release tarballs are available at:
329 https://www.kernel.org/pub/software/scm/git/
333 To: git\@vger.kernel.org
335 Subject: What's cooking in git.git ($mon $year, #$issue; $dow, $date)
336 X-$MASTER-at: $master_at
339 What's cooking in git.git ($mon $year, #$issue; $dow, $date)
340 --------------------------------------------------
344 $text =~ s/\n+\Z/\n/;
348 my $blurb_match = <<'EOF';
349 (?:(?i:\s*[a-z]+: .*|\s.*)\n)*?Subject: What's cooking in \S+ \((\w+) (\d+), #(\d+); (\w+), (\d+)\)
350 X-[a-z]*-at: ([0-9a-f]{40})
351 X-next-at: ([0-9a-f]{40})
353 What's cooking in \S+ \(\1 \2, #\3; \4, \5\)
358 my $blurb = "b..l..u..r..b";
365 my $last_empty = undef;
366 my (@section, %section, @branch, %branch, %description, @leader);
367 my $in_unedited_olde = 0;
371 'section_list' => [],
372 'section_data' => {},
373 'topic_description' => {
376 text => blurb_text(),
382 open ($fh, '<', $fn) or die "$!: open $fn";
386 if ($in_unedited_olde) {
388 $in_unedited_olde = 0;
392 $in_unedited_olde = 1;
395 if ($in_unedited_olde) {
399 if (defined $section && /^-{20,}$/) {
406 if (/^\[(.*)\]\s*$/) {
409 if (!exists $section{$section}) {
410 push @section, $section;
411 $section{$section} = [];
415 if (defined $section && /^\* (\S+) /) {
418 if (!exists $branch{$branch}) {
419 push @branch, [$branch, $section];
420 $branch{$branch} = 1;
422 push @{$section{$section}}, $branch;
424 if (defined $branch) {
425 my $was_last_empty = $last_empty;
427 if (!exists $description{$branch}) {
428 $description{$branch} = [];
430 if ($was_last_empty) {
431 push @{$description{$branch}}, "";
433 push @{$description{$branch}}, $_;
439 for my $branch (keys %description) {
440 my $ary = $description{$branch};
441 if ($branch eq $blurb) {
442 while (@{$ary} && $ary->[-1] =~ /^-{30,}$/) {
445 $description{$branch} = +{
447 text => join("\n", @{$ary}),
452 my $elem = shift @{$ary};
453 last if ($elem eq '');
463 $description{$branch} = +{
464 desc => join("\n", @desc),
465 text => join("\n", @txt),
471 section_list => \@section,
472 section_data => \%section,
473 topic_description => \%description,
478 my ($fn, $cooking) = @_;
481 open($fh, '>', $fn) or die "$!: open $fn";
482 print $fh $cooking->{'topic_description'}{$blurb}{'text'};
484 for my $section_name (@{$cooking->{'section_list'}}) {
485 my $topic_list = $cooking->{'section_data'}{$section_name};
486 next if (!@{$topic_list});
489 print $fh '-' x 50, "\n";
490 print $fh "[$section_name]\n";
492 for my $topic (@{$topic_list}) {
493 my $d = $cooking->{'topic_description'}{$topic};
495 print $fh $lead, $d->{'desc'}, "\n";
497 # Final clean-up. No leading or trailing
498 # blank lines, no multi-line gaps.
504 print $fh "\n", $d->{'text'}, "\n";
512 my $graduated = "Graduated to '$MASTER'";
513 my $new_topics = 'New Topics';
514 my $discarded = 'Discarded';
515 my $cooking_topics = 'Cooking';
519 my ($fh, $master_at, $next_at, $incremental);
522 qw(git for-each-ref),
523 "--format=%(refname:short) %(objectname)",
524 "refs/heads/$MASTER",
525 "refs/heads/next") or die "$!: open for-each-ref";
527 my ($branch, $at) = /^(\S+) (\S+)$/;
528 if ($branch eq $MASTER) { $master_at = $at; }
529 if ($branch eq 'next') { $next_at = $at; }
531 close($fh) or die "$!: close for-each-ref";
533 $incremental = ((-r "Meta/whats-cooking.txt") &&
534 system("cd Meta && " .
535 "git diff --quiet --no-ext-diff HEAD -- " .
536 "whats-cooking.txt"));
538 my $now_string = localtime;
539 my ($current_dow, $current_mon, $current_date, $current_year) =
540 ($now_string =~ /^(\w+) (\w+) +(\d+) [\d:]+ (\d+)$/);
542 my $btext = $cooking->{'topic_description'}{$blurb}{'text'};
543 if ($btext !~ s/\A$blurb_match//) {
544 die "match pattern broken?";
546 my ($mon, $year, $issue, $dow, $date) = ($1, $2, $3, $4, $5);
548 if ($current_mon ne $mon || $current_year ne $year) {
550 } elsif (!$incremental) {
552 $issue = sprintf "%02d", ($issue + 1);
555 $year = $current_year;
557 $date = $current_date;
559 $cooking->{'topic_description'}{$blurb}{'text'} =
560 blurb_text($mon, $year, $issue, $dow, $date,
561 $master_at, $next_at, $btext);
563 # If starting a new issue, move what used to be in
564 # new topics to cooking topics.
566 my $sd = $cooking->{'section_data'};
567 my $sl = $cooking->{'section_list'};
569 if (exists $sd->{$new_topics}) {
570 if (!exists $sd->{$cooking_topics}) {
571 $sd->{$cooking_topics} = [];
572 unshift @{$sl}, $cooking_topics;
574 unshift @{$sd->{$cooking_topics}}, @{$sd->{$new_topics}};
576 $sd->{$new_topics} = [];
583 my ($topic_desc) = @_;
584 for my $line (split(/\n/, $topic_desc)) {
585 if ($line =~ /^ [+-] /) {
596 my $desc = $td->{'desc'};
597 my $text = $td->{'text'};
599 if (!defined $mergetomaster) {
600 my $master = `git describe $MASTER`;
601 if ($master =~ /-rc\d+(-\d+-g[0-9a-f]+)?$/) {
602 $mergetomaster = "Will cook in 'next'.";
604 $mergetomaster = "Will merge to '$MASTER'.";
608 # If updated description (i.e. the list of patches with
609 # merge trail to 'next') has 'merged to next', then
610 # tweak the topic to be slated to 'master'.
611 # NEEDSWORK: does this work correctly for a half-merged topic?
612 $desc =~ s/\n<<\n.*//s;
613 if ($desc =~ /^ \(merged to 'next'/m) {
614 $text =~ s/^ Will merge to 'next'\.$/ $mergetomaster/m;
615 $text =~ s/^ Will merge to and (then )?cook in 'next'\.$/ Will cook in 'next'./m;
616 $text =~ s/^ Will merge to 'next' and (then )?to '$MASTER'\.$/ Will merge to '$MASTER'./m;
618 $td->{'text'} = $text;
621 sub tweak_graduated {
624 # Remove the "Will merge" marker from topics that have graduated.
625 for ($td->{'text'}) {
626 s/\n Will merge to '$MASTER'\.(\n|$)//s;
631 my ($cooking, $current) = @_;
633 # A hash to find <desc, text> with a branch name or $blurb
634 my $td = $cooking->{'topic_description'};
636 # A hash to find a list of $td element given a section name
637 my $sd = $cooking->{'section_data'};
639 # A list of section names
640 my $sl = $cooking->{'section_list'};
642 my (@new_topic, @gone_topic);
644 # Make sure "New Topics" and "Graduated" exists
645 if (!exists $sd->{$new_topics}) {
646 $sd->{$new_topics} = [];
647 unshift @{$sl}, $new_topics;
650 if (!exists $sd->{$graduated}) {
651 $sd->{$graduated} = [];
652 unshift @{$sl}, $graduated;
655 my $incremental = update_issue($cooking);
657 for my $topic (sort keys %{$current}) {
658 if (!exists $td->{$topic}) {
659 # Ignore new topics without anything merged
660 if (topic_in_seen($current->{$topic}{'desc'})) {
661 push @new_topic, $topic;
665 # Annotate if the contents of the topic changed
666 my $n = $current->{$topic}{'desc'};
667 my $o = $td->{$topic}{'desc'};
669 $td->{$topic}{'desc'} = $n . "\n<<\n" . $o ."\n>>";
670 tweak_willdo($td->{$topic});
674 for my $topic (sort keys %{$td}) {
675 next if ($topic eq $blurb);
676 next if (!$incremental &&
677 grep { $topic eq $_ } @{$sd->{$graduated}});
678 next if (grep { $topic eq $_ } @{$sd->{$discarded}});
679 if (!exists $current->{$topic}) {
680 push @gone_topic, $topic;
685 push @{$sd->{$new_topics}}, $_;
686 $td->{$_}{'desc'} = $current->{$_}{'desc'};
690 $sd->{$graduated} = [];
694 for my $topic (@gone_topic) {
695 for my $section (@{$sl}) {
696 my $pre = scalar(@{$sd->{$section}});
697 @{$sd->{$section}} = (grep { $_ ne $topic }
699 my $post = scalar(@{$sd->{$section}});
700 next if ($pre == $post);
704 push @{$sd->{$graduated}}, $_;
705 tweak_graduated($td->{$_});
710 ################################################################
713 my ($what, $action, $topic) = @_;
714 if (!exists $what->{$action}) {
715 $what->{$action} = [];
717 push @{$what->{$action}}, $topic;
724 return if (/^Graduated to/ || /^Discarded$/);
725 return $_ if (/^Stalled$/);
731 sub wildo_flush_topic {
732 my ($in_section, $what, $topic) = @_;
733 if (defined $topic) {
734 my $action = section_action($in_section);
736 wildo_queue($what, $action, $topic);
742 # NEEDSWORK: unify with Reintegrate::annotate_merge
743 if (/^Will (?:\S+ ){0,2}(fast-track|hold|keep|merge|drop|discard|cook|kick|defer|eject|be re-?rolled|wait)[,. ]/ ||
744 /^Not urgent/ || /^Not ready/ || /^Waiting for / ||
745 /^Can wait in / || /^Still / || /^Stuck / || /^On hold/ ||
746 /^Needs? / || /^Expecting / || /^May want to /) {
749 if (/^I think this is ready for /) {
757 my (%what, $topic, $last_merge_to_next, $in_section, $in_desc);
758 my $too_recent = '9999-99-99';
763 my $old_section = $in_section;
765 wildo_flush_topic($old_section, \%what, $topic);
766 $topic = $in_desc = undef;
770 if (/^\* (\S+) \(([-0-9]+)\) (\d+) commits?$/) {
771 wildo_flush_topic($in_section, \%what, $topic);
773 # tip-date, next-date, topic, count, seen-count
774 $topic = [$2, $too_recent, $1, $3, 0];
779 if (defined $topic &&
780 ($topic->[1] eq $too_recent) &&
781 ($topic->[4] == 0) &&
782 (/^ \(merged to 'next' on ([-0-9]+)/)) {
785 if (defined $topic && /^ - /) {
789 if (defined $topic && /^$/) {
794 next unless defined $topic && $in_desc;
797 if (wildo_match($_)) {
798 wildo_queue(\%what, $_, $topic);
799 $topic = $in_desc = undef;
802 if (/Originally merged to 'next' on ([-0-9]+)/) {
806 wildo_flush_topic($in_section, \%what, $topic);
809 for my $what (sort keys %what) {
810 print "$ipbl$what\n";
811 for $topic (sort { (($a->[1] cmp $b->[1]) ||
812 ($a->[0] cmp $b->[0])) }
814 my ($tip, $next, $name, $count, $seen) = @$topic;
817 if (($next eq $too_recent) || (0 < $seen)) {
822 $next =~ s|^\d{4}-|/|;
825 printf " %s %-60s %s%s %5s\n", $sign, $name, $tip, $next, $count;
831 ################################################################
841 for $str (split(/\n/, $str)) {
842 print "$prefix$str\n";
849 my $cnt = `git rev-list --count @range`;
858 my ($topic, $to_maint, %to_maint, %merged, $in_desc);
861 qw(git rev-list --first-parent -1), $MASTER,
862 qw(-- Documentation/RelNotes RelNotes))
863 or die "$!: open rev-list";
865 close($fh) or die "$!: close rev-list";
867 @ARGV = ("$rev..$MASTER");
870 qw(git log --first-parent --oneline --reverse), @ARGV)
871 or die "$!: open log --first-parent";
873 my ($sha1, $branch) = /^([0-9a-f]+) Merge branch '(.*)'$/;
875 $topic{$branch} = "";
876 $merged{$branch} = $sha1;
877 push @topic, $branch;
879 close($fh) or die "$!: close log --first-parent";
880 open($fh, "<", "Meta/whats-cooking.txt")
881 or die "$!: open whats-cooking";
886 $in_desc = $topic = undef;
889 if (/^\* (\S+) \([-0-9]+\) \d+ commits?$/) {
890 if (exists $topic{$1}) {
894 $in_desc = $topic = undef;
898 if (defined $topic && /^$/) {
903 next unless defined $topic && $in_desc;
906 if (wildo_match($_)) {
909 $topic{$topic} .= "$_\n";
911 close($fh) or die "$!: close whats-cooking";
913 for $topic (@topic) {
914 my $merged = $merged{$topic};
915 my $in_master = havedone_count("$merged^1..$merged^2");
916 my $not_in_maint = havedone_count("maint..$merged^2");
917 if ($in_master == $not_in_maint) {
918 $to_maint{$topic} = 1;
923 for $topic (@topic) {
924 next if (exists $to_maint{$topic});
925 havedone_show($topic, $topic{$topic});
931 print "-" x 64, "\n";
934 for $topic (@topic) {
935 next unless (exists $to_maint{$topic});
936 havedone_show($topic, $topic{$topic});
937 my $sha1 = `git rev-parse --short $topic`;
939 print " (merge $sha1 $topic later to maint).\n";
944 ################################################################
948 my $topic = get_commit();
949 my $cooking = read_previous('Meta/whats-cooking.txt');
950 merge_cooking($cooking, $topic);
951 write_cooking('Meta/whats-cooking.txt', $cooking);
954 ################################################################
959 my ($wildo, $havedone);
960 if (!GetOptions("wildo" => \$wildo,
961 "havedone" => \$havedone)) {
962 print STDERR "$0 [--wildo|--havedone]\n";
969 open($fd, "<", "Meta/whats-cooking.txt");
970 } elsif (@ARGV != 1) {
971 print STDERR "$0 --wildo [filename|HEAD|-]\n";
973 } elsif ($ARGV[0] eq '-') {
975 } elsif ($ARGV[0] =~ /^HEAD/) {
977 qw(git --git-dir=Meta/.git cat-file -p),
978 "$ARGV[0]:whats-cooking.txt");
979 } elsif ($ARGV[0] eq ":") {
981 qw(git --git-dir=Meta/.git cat-file -p),
982 ":whats-cooking.txt");
984 open($fd, "<", $ARGV[0]);
987 } elsif ($havedone) {