1#!/usr/bin/perl 2# 3# This is a rough draft of a tool to aid in generating a perldelta file 4# from a series of git commits. 5 6use 5.010; 7use strict; 8use warnings; 9package Git::DeltaTool; 10 11use Class::Struct; 12use File::Basename; 13use File::Temp; 14use Getopt::Long; 15use Git::Wrapper; 16use Term::ReadKey; 17use Term::ANSIColor; 18use Pod::Usage; 19 20BEGIN { struct( git => '$', last_tag => '$', opt => '%', original_stdout => '$' ) } 21 22__PACKAGE__->run; 23 24#--------------------------------------------------------------------------# 25# main program 26#--------------------------------------------------------------------------# 27 28sub run { 29 my $class = shift; 30 31 my %opt = ( 32 mode => 'assign', 33 ); 34 35 GetOptions( \%opt, 36 # inputs 37 'mode|m:s', # 'assign', 'review', 'render', 'update' 38 'type|t:s', # select by status 39 'status|s:s', # status to set for 'update' 40 'since:s', # origin commit 41 'help|h', # help 42 ); 43 44 pod2usage() if $opt{help}; 45 46 my $git = Git::Wrapper->new("."); 47 my $git_id = $opt{since}; 48 if ( defined $git_id ) { 49 die "Invalid git identifier '$git_id'\n" 50 unless eval { $git->show($git_id); 1 }; 51 } else { 52 ($git_id) = $git->describe; 53 $git_id =~ s/-.*$//; 54 } 55 my $gdt = $class->new( git => $git, last_tag => $git_id, opt => \%opt ); 56 57 if ( $opt{mode} eq 'assign' ) { 58 $opt{type} //= 'new'; 59 $gdt->assign; 60 } 61 elsif ( $opt{mode} eq 'review' ) { 62 $opt{type} //= 'pending'; 63 $gdt->review; 64 } 65 elsif ( $opt{mode} eq 'render' ) { 66 $opt{type} //= 'pending'; 67 $gdt->render; 68 } 69 elsif ( $opt{mode} eq 'summary' ) { 70 $opt{type} //= 'pending'; 71 $gdt->summary; 72 } 73 elsif ( $opt{mode} eq 'update' ) { 74 die "Explicit --type argument required for update mode\n" 75 unless defined $opt{type}; 76 die "Explicit --status argument required for update mode\n" 77 unless defined $opt{status}; 78 $gdt->update; 79 } 80 else { 81 die "Unrecognized mode '$opt{mode}'\n"; 82 } 83 exit 0; 84} 85 86#--------------------------------------------------------------------------# 87# program modes (and iterator) 88#--------------------------------------------------------------------------# 89 90sub assign { 91 my ($self) = @_; 92 my @choices = ( $self->section_choices, $self->action_choices ); 93 $self->_iterate_commits( 94 sub { 95 my ($log, $i, $count) = @_; 96 say "\n### Commit @{[$i+1]} of $count ###"; 97 say "-" x 75; 98 $self->show_header($log); 99 $self->show_body($log, 1); 100 $self->show_files($log); 101 say "-" x 75; 102 return $self->dispatch( $self->prompt( @choices ), $log); 103 } 104 ); 105 return; 106} 107 108sub review { 109 my ($self) = @_; 110 my @choices = ( $self->review_choices, $self->action_choices ); 111 $self->_iterate_commits( 112 sub { 113 my ($log, $i, $count) = @_; 114 say "\n### Commit @{[$i+1]} of $count ###"; 115 say "-" x 75; 116 $self->show_header($log); 117 $self->show_notes($log, 1); 118 say "-" x 75; 119 return $self->dispatch( $self->prompt( @choices ), $log); 120 } 121 ); 122 return; 123} 124 125sub render { 126 my ($self) = @_; 127 my %sections; 128 $self->_iterate_commits( 129 sub { 130 my $log = shift; 131 my $section = $self->note_section($log) or return; 132 push @{ $sections{$section} }, $self->note_delta($log); 133 return 1; 134 } 135 ); 136 my @order = $self->section_order; 137 my %known = map { $_ => 1 } @order; 138 my @rest = grep { ! $known{$_} } keys %sections; 139 for my $s ( @order, @rest ) { 140 next unless ref $sections{$s}; 141 say "-"x75; 142 say uc($s) . "\n"; 143 say join ( "\n", @{ $sections{$s} }, "" ); 144 } 145 return; 146} 147 148sub summary { 149 my ($self) = @_; 150 $self->_iterate_commits( 151 sub { 152 my $log = shift; 153 $self->show_header($log); 154 return 1; 155 } 156 ); 157 return; 158} 159 160sub update { 161 my ($self) = @_; 162 163 my $status = $self->opt('status') 164 or die "The 'status' option must be supplied for update mode\n"; 165 166 $self->_iterate_commits( 167 sub { 168 my $log = shift; 169 my $note = $log->notes; 170 $note =~ s{^(perldelta.*\[)\w+(\].*)}{$1$status$2}ms; 171 $self->add_note( $log->id, $note ); 172 return 1; 173 } 174 ); 175 return; 176} 177 178sub _iterate_commits { 179 my ($self, $fcn) = @_; 180 my $type = $self->opt('type'); 181 say STDERR "Scanning for $type commits since " . $self->last_tag . "..."; 182 my $list = [ $self->find_commits($type) ]; 183 my $count = @$list; 184 while ( my ($i,$log) = each @$list ) { 185 redo unless $fcn->($log, $i, $count); 186 } 187 return 1; 188} 189 190#--------------------------------------------------------------------------# 191# methods 192#--------------------------------------------------------------------------# 193 194sub add_note { 195 my ($self, $id, $note) = @_; 196 my @lines = split "\n", _strip_comments($note); 197 pop @lines while @lines && $lines[-1] =~ m{^\s*$}; 198 my $tempfh = File::Temp->new; 199 if (@lines) { 200 $tempfh->printflush( join( "\n", @lines), "\n" ); 201 $self->git->notes('edit', '-F', "$tempfh", $id); 202 } 203 else { 204 $tempfh->printflush( "\n" ); 205 # git notes won't take an empty file as input 206 system("git notes edit -F $tempfh $id"); 207 } 208 209 return; 210} 211 212sub dispatch { 213 my ($self, $choice, $log) = @_; 214 return unless $choice; 215 my $method = "do_$choice->{handler}"; 216 return 1 unless $self->can($method); # missing methods "succeed" 217 return $self->$method($choice, $log); 218} 219 220sub edit_text { 221 my ($self, $text, $args) = @_; 222 $args //= {}; 223 my $tempfh = File::Temp->new; 224 $tempfh->printflush( $text ); 225 if ( my @editor = split /\s+/, ($ENV{VISUAL} || $ENV{EDITOR}) ) { 226 push @editor, "-f" if $editor[0] =~ /^gvim/; 227 system(@editor, "$tempfh"); 228 } 229 else { 230 warn("No VISUAL or EDITOR defined"); 231 } 232 return do { local (@ARGV,$/) = "$tempfh"; <> }; 233} 234 235sub find_commits { 236 my ($self, $type) = @_; 237 $type //= 'new'; 238 my @commits = $self->git->log($self->last_tag . "..HEAD"); 239 $_ = Git::Wrapper::XLog->from_log($_, $self->git) for @commits; 240 my @list; 241 if ( $type eq 'new' ) { 242 @list = grep { ! $_->notes } @commits; 243 } 244 else { 245 @list = grep { $self->note_status( $_ ) eq $type } @commits; 246 } 247 return @list; 248} 249 250sub get_diff { 251 my ($self, $log) = @_; 252 my @diff = $self->git->show({ stat => 1, p => 1 }, $log->id); 253 return join("\n", @diff); 254} 255 256sub note_delta { 257 my ($self, $log) = @_; 258 my @delta = split "\n", ($log->notes || ''); 259 return '' unless @delta; 260 splice @delta, 0, 2; 261 return join( "\n", @delta, "" ); 262} 263 264sub note_section { 265 my ($self, $log) = @_; 266 my $note = $log->notes or return ''; 267 my ($section) = $note =~ m{^perldelta:\s*([^\[]*)\s+}ms; 268 return $section || ''; 269} 270 271sub note_status { 272 my ($self, $log) = @_; 273 my $note = $log->notes or return ''; 274 my ($status) = $note =~ m{^perldelta:\s*[^\[]*\[(\w+)\]}ms; 275 return $status || ''; 276} 277 278sub note_template { 279 my ($self, $log, $text) = @_; 280 my $diff = _prepend_comment( $self->get_diff($log) ); 281 return << "HERE"; 282# Edit commit note below. Do not change the first line. Comments are stripped 283$text 284 285$diff 286HERE 287} 288 289sub prompt { 290 my ($self, @choices) = @_; 291 my ($valid, @menu, %keymap) = ''; 292 for my $c ( map { @$_ } @choices ) { 293 my ($item) = grep { /\(/ } split q{ }, $c->{name}; 294 my ($button) = $item =~ m{\((.)\)}; 295 die "No key shortcut found for '$item'" unless $button; 296 die "Duplicate key shortcut found for '$item'" if $keymap{lc $button}; 297 push @menu, $item; 298 $valid .= lc $button; 299 $keymap{lc $button} = $c; 300 } 301 my $keypress = $self->prompt_key( $self->wrap_list(@menu), $valid ); 302 return $keymap{lc $keypress}; 303} 304 305sub prompt_key { 306 my ($self, $prompt, $valid_keys) = @_; 307 my $key; 308 KEY: { 309 say $prompt; 310 ReadMode 3; 311 $key = lc ReadKey(0); 312 ReadMode 0; 313 if ( $key !~ qr/\A[$valid_keys]\z/i ) { 314 say ""; 315 redo KEY; 316 } 317 } 318 return $key; 319} 320 321sub show_body { 322 my ($self, $log, $lf) = @_; 323 return unless my $body = $log->body; 324 say $lf ? "\n$body" : $body; 325 return; 326} 327 328sub show_files { 329 my ($self, $log) = @_; 330 my @files = $self->git->diff_tree({r => 1, abbrev => 1}, $log->id); 331 shift @files; # throw away commit line 332 return unless @files; 333 say "\nChanged:"; 334 say join("\n", map { " * $_" } sort map { /.*\s+(\S+)/; $1 } @files); 335 return; 336} 337 338sub show_header { 339 my ($self, $log) = @_; 340 my $header = $log->short_id; 341 $header .= " " . $log->subject if length $log->subject; 342 $header .= sprintf(' (%s)', $log->author) if $log->author; 343 say colored( $header, "yellow"); 344 return; 345} 346 347sub show_notes { 348 my ($self, $log, $lf) = @_; 349 return unless my $notes = $log->notes; 350 say $lf ? "\n$notes" : $notes; 351 return; 352} 353 354sub wrap_list { 355 my ($self, @list) = @_; 356 my $line = shift @list; 357 my @wrap; 358 for my $item ( @list ) { 359 if ( length( $line . $item ) > 70 ) { 360 push @wrap, $line; 361 $line = $item ne $list[-1] ? $item : "or $item"; 362 } 363 else { 364 $line .= $item ne $list[-1] ? ", $item" : " or $item"; 365 } 366 } 367 return join("\n", @wrap, $line); 368} 369 370sub y_n { 371 my ($self, $msg) = @_; 372 my $key = $self->prompt_key($msg . " (y/n?)", 'yn'); 373 return $key eq 'y'; 374} 375 376#--------------------------------------------------------------------------# 377# handlers 378#--------------------------------------------------------------------------# 379 380sub do_blocking { 381 my ($self, $choice, $log) = @_; 382 my $note = "perldelta: Unknown [blocking]\n"; 383 $self->add_note( $log->id, $note ); 384 return 1; 385} 386 387sub do_examine { 388 my ($self, $choice, $log) = @_; 389 $self->start_pager; 390 say $self->get_diff($log); 391 $self->end_pager; 392 return; 393} 394 395sub do_cherry { 396 my ($self, $choice, $log) = @_; 397 my $id = $log->short_id; 398 $self->y_n("Recommend a cherry pick of '$id' to maint?") or return; 399 my $cherrymaint = dirname($0) . "/cherrymaint"; 400 system("$^X $cherrymaint --vote $id"); 401 return; # false will re-prompt the same commit 402} 403 404sub do_done { 405 my ($self, $choice, $log) = @_; 406 my $note = $log->notes; 407 $note =~ s{^(perldelta.*\[)\w+(\].*)}{$1done$2}ms; 408 $self->add_note( $log->id, $note ); 409 return 1; 410} 411 412sub do_edit { 413 my ($self, $choice, $log) = @_; 414 my $old_note = $log->notes; 415 my $new_note = $self->edit_text( $self->note_template( $log, $old_note) ); 416 $self->add_note( $log->id, $new_note ); 417 return 1; 418} 419 420sub do_head2 { 421 my ($self, $choice, $log) = @_; 422 my $section = _strip_parens($choice->{name}); 423 my $subject = $log->subject; 424 my $body = $log->body; 425 426 my $template = $self->note_template( $log, 427 "perldelta: $section [pending]\n\n=head2 $subject\n\n$body\n" 428 ); 429 430 my $note = $self->edit_text( $template ); 431 if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 432 $self->add_note( $log->id, $note ); 433 return 1; 434 } 435 return; 436} 437 438sub do_linked_item { 439 my ($self, $choice, $log) = @_; 440 my $section = _strip_parens($choice->{name}); 441 my $subject = $log->subject; 442 my $body = $log->body; 443 444 my $template = $self->note_template( $log, 445 "perldelta: $section [pending]\n\n=head3 L<LINK>\n\n=over\n\n=item *\n\n$subject\n\n$body\n\n=back\n" 446 ); 447 448 my $note = $self->edit_text($template); 449 if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 450 $self->add_note( $log->id, $note ); 451 return 1; 452 } 453 return; 454} 455 456sub do_item { 457 my ($self, $choice, $log) = @_; 458 my $section = _strip_parens($choice->{name}); 459 my $subject = $log->subject; 460 my $body = $log->body; 461 462 my $template = $self->note_template( $log, 463 "perldelta: $section [pending]\n\n=item *\n\n$subject\n\n$body\n" 464 ); 465 466 my $note = $self->edit_text($template); 467 if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 468 $self->add_note( $log->id, $note ); 469 return 1; 470 } 471 return; 472} 473 474sub do_none { 475 my ($self, $choice, $log) = @_; 476 my $note = "perldelta: None [ignored]\n"; 477 $self->add_note( $log->id, $note ); 478 return 1; 479} 480 481sub do_platform { 482 my ($self, $choice, $log) = @_; 483 my $section = _strip_parens($choice->{name}); 484 my $subject = $log->subject; 485 my $body = $log->body; 486 487 my $template = $self->note_template( $log, 488 "perldelta: $section [pending]\n\n=item PLATFORM-NAME\n\n$subject\n\n$body\n" 489 ); 490 491 my $note = $self->edit_text($template); 492 if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 493 $self->add_note( $log->id, $note ); 494 return 1; 495 } 496 return; 497} 498 499sub do_quit { exit 0 } 500 501sub do_repeat { return 0 } 502 503sub do_skip { return 1 } 504 505sub do_special { 506 my ($self, $choice, $log) = @_; 507 my $section = _strip_parens($choice->{name}); 508 my $subject = $log->subject; 509 my $body = $log->body; 510 511 my $template = $self->note_template( $log, << "HERE" ); 512perldelta: $section [pending] 513 514$subject 515 516$body 517HERE 518 519 my $note = $self->edit_text( $template ); 520 if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 521 $self->add_note( $log->id, $note ); 522 return 1; 523 } 524 return; 525} 526 527sub do_subsection { 528 my ($self, $choice, $log) = @_; 529 my @choices = ( $choice->{subsection}, $self->submenu_choices ); 530 say "For " . _strip_parens($choice->{name}) . ":"; 531 return $self->dispatch( $self->prompt( @choices ), $log); 532} 533 534#--------------------------------------------------------------------------# 535# define prompts 536#--------------------------------------------------------------------------# 537 538sub action_choices { 539 my ($self) = @_; 540 state $action_choices = [ 541 { name => 'E(x)amine', handler => 'examine' }, 542 { name => '(+)Cherrymaint', handler => 'cherry' }, 543 { name => '(?)NeedHelp', handler => 'blocking' }, 544 { name => 'S(k)ip', handler => 'skip' }, 545 { name => '(Q)uit', handler => 'quit' }, 546 ]; 547 return $action_choices; 548} 549 550sub submenu_choices { 551 my ($self) = @_; 552 state $submenu_choices = [ 553 { name => '(B)ack', handler => 'repeat' }, 554 ]; 555 return $submenu_choices; 556} 557 558 559sub review_choices { 560 my ($self) = @_; 561 state $action_choices = [ 562 { name => '(E)dit', handler => 'edit' }, 563 { name => '(I)gnore', handler => 'none' }, 564 { name => '(D)one', handler => 'done' }, 565 ]; 566 return $action_choices; 567} 568 569sub section_choices { 570 my ($self, $key) = @_; 571 state $section_choices = [ 572 # Headline stuff that should go first 573 { 574 name => 'Core (E)nhancements', 575 handler => 'head2', 576 }, 577 { 578 name => 'Securit(y)', 579 handler => 'head2', 580 }, 581 { 582 name => '(I)ncompatible Changes', 583 handler => 'head2', 584 }, 585 { 586 name => 'Dep(r)ecations', 587 handler => 'head2', 588 }, 589 { 590 name => '(P)erformance Enhancements', 591 handler => 'item', 592 }, 593 594 # Details on things installed with Perl (for Perl developers) 595 { 596 name => '(M)odules and Pragmata', 597 handler => 'subsection', 598 subsection => [ 599 { 600 name => '(N)ew Modules and Pragmata', 601 handler => 'item', 602 }, 603 { 604 name => '(U)pdated Modules and Pragmata', 605 handler => 'item', 606 }, 607 { 608 name => '(R)emoved Modules and Pragmata', 609 handler => 'item', 610 }, 611 ], 612 }, 613 { 614 name => '(D)ocumentation', 615 handler => 'subsection', 616 subsection => [ 617 { 618 name => '(N)ew Documentation', 619 handler => 'linked_item', 620 }, 621 { 622 name => '(C)hanges to Existing Documentation', 623 handler => 'linked_item', 624 }, 625 ], 626 }, 627 { 628 name => 'Dia(g)nostics', 629 handler => 'subsection', 630 subsection => [ 631 { 632 name => '(N)ew Diagnostics', 633 handler => 'item', 634 }, 635 { 636 name => '(C)hanges to Existing Diagnostics', 637 handler => 'item', 638 }, 639 ], 640 }, 641 { 642 name => '(U)tilities', 643 handler => 'linked_item', 644 }, 645 646 # Details on building/testing Perl (for porters and packagers) 647 { 648 name => '(C)onfiguration and Compilation', 649 handler => 'item', 650 }, 651 { 652 name => '(T)esting', # new tests or significant notes about it 653 handler => 'item', 654 }, 655 { 656 name => 'Pl(a)tform Support', 657 handler => 'subsection', 658 subsection => [ 659 { 660 name => '(N)ew Platforms', 661 handler => 'platform', 662 }, 663 { 664 name => '(D)iscontinued Platforms', 665 handler => 'platform', 666 }, 667 { 668 name => '(P)latform-Specific Notes', 669 handler => 'platform', 670 }, 671 ], 672 }, 673 674 # Details on perl internals (for porters and XS developers) 675 { 676 name => 'Inter(n)al Changes', 677 handler => 'item', 678 }, 679 680 # Bugs fixed and related stuff 681 { 682 name => 'Selected Bug (F)ixes', 683 handler => 'item', 684 }, 685 { 686 name => 'Known Prob(l)ems', 687 handler => 'item', 688 }, 689 690 # dummy options for special handling 691 { 692 name => '(S)pecial', 693 handler => 'special', 694 }, 695 { 696 name => '(*)None', 697 handler => 'none', 698 }, 699 ]; 700 return $section_choices; 701} 702 703sub section_order { 704 my ($self) = @_; 705 state @order; 706 if ( ! @order ) { 707 for my $c ( @{ $self->section_choices } ) { 708 if ( $c->{subsection} ) { 709 push @order, map { $_->{name} } @{$c->{subsection}}; 710 } 711 else { 712 push @order, $c->{name}; 713 } 714 } 715 } 716 return @order; 717} 718 719#--------------------------------------------------------------------------# 720# Pager handling 721#--------------------------------------------------------------------------# 722 723sub get_pager { $ENV{'PAGER'} || `which less` || `which more` } 724 725sub in_pager { shift->original_stdout ? 1 : 0 } 726 727sub start_pager { 728 my $self = shift; 729 my $content = shift; 730 if (!$self->in_pager) { 731 local $ENV{'LESS'} ||= '-FXe'; 732 local $ENV{'MORE'}; 733 $ENV{'MORE'} ||= '-FXe' unless $^O =~ /^MSWin/; 734 735 my $pager = $self->get_pager; 736 return unless $pager; 737 open (my $cmd, "|-", $pager) || return; 738 $|++; 739 $self->original_stdout(*STDOUT); 740 741 # $pager will be closed once we restore STDOUT to $original_stdout 742 *STDOUT = $cmd; 743 } 744} 745 746sub end_pager { 747 my $self = shift; 748 return unless ($self->in_pager); 749 *STDOUT = $self->original_stdout; 750 751 # closes the pager 752 $self->original_stdout(undef); 753} 754 755#--------------------------------------------------------------------------# 756# Utility functions 757#--------------------------------------------------------------------------# 758 759sub _strip_parens { 760 my ($name) = @_; 761 $name =~ s/[()]//g; 762 return $name; 763} 764 765sub _prepend_comment { 766 my ($text) = @_; 767 return join ("\n", map { s/^/# /g; $_ } split "\n", $text); 768} 769 770sub _strip_comments { 771 my ($text) = @_; 772 return join ("\n", grep { ! /^#/ } split "\n", $text); 773} 774 775#--------------------------------------------------------------------------# 776# Extend Git::Wrapper::Log 777#--------------------------------------------------------------------------# 778 779package Git::Wrapper::XLog; 780BEGIN { our @ISA = qw/Git::Wrapper::Log/; } 781 782sub subject { shift->attr->{subject} } 783sub body { shift->attr->{body} } 784sub short_id { shift->attr->{short_id} } 785sub author { shift->attr->{author} } 786 787sub from_log { 788 my ($class, $log, $git) = @_; 789 790 my $msg = $log->message; 791 my ($subject, $body) = $msg =~ m{^([^\n]+)\n*(.*)}ms; 792 $subject //= ''; 793 $body //= ''; 794 $body =~ s/[\r\n]*\z//ms; 795 796 my ($short) = $git->rev_parse({short => 1}, $log->id); 797 798 $log->attr->{subject} = $subject; 799 $log->attr->{body} = $body; 800 $log->attr->{short_id} = $short; 801 return bless $log, $class; 802} 803 804sub notes { 805 my ($self) = @_; 806 my @notes = eval { Git::Wrapper->new(".")->notes('show', $self->id) }; 807 pop @notes while @notes && $notes[-1] =~ m{^\s*$}; 808 return unless @notes; 809 return join ("\n", @notes); 810} 811 812__END__ 813 814=head1 NAME 815 816git-deltatool - Annotate commits for perldelta 817 818=head1 SYNOPSIS 819 820 # annotate commits back to last 'git describe' tag 821 822 $ git-deltatool 823 824 # review annotations 825 826 $ git-deltatool --mode review 827 828 # review commits needing help 829 830 $ git-deltatool --mode review --type blocking 831 832 # summarize commits needing help 833 834 $ git-deltatool --mode summary --type blocking 835 836 # assemble annotations by section to STDOUT 837 838 $ git-deltatool --mode render 839 840 # Get a list of commits needing further review, e.g. for peer review 841 842 $ git-deltatool --mode summary --type blocking 843 844 # mark 'pending' annotations as 'done' (i.e. added to perldelta) 845 846 $ git-deltatool --mode update --type pending --status done 847 848=head1 OPTIONS 849 850=over 851 852=item B<--mode>|B<-m> MODE 853 854Indicates the run mode for the program. The default is 'assign' which 855assigns categories and marks the notes as 'pending' (or 'ignored'). Other 856modes are 'review', 'render', 'summary' and 'update'. 857 858=item B<--type>|B<-t> TYPE 859 860Indicates what types of commits to process. The default for 'assign' mode is 861'new', which processes commits without any perldelta notes. The default for 862'review', 'summary' and 'render' modes is 'pending'. The options must be set 863explicitly for 'update' mode. 864 865The type 'blocking' is reserved for commits needing further review. 866 867=item B<--status>|B<-s> STATUS 868 869For 'update' mode only, sets a new status. While there is no restriction, 870it should be one of 'new', 'pending', 'blocking', 'ignored' or 'done'. 871 872=item B<--since> REVISION 873 874Defines the boundary for searching git commits. Defaults to the last 875major tag (as would be given by 'git describe'). 876 877=item B<--help> 878 879Shows the manual. 880 881=back 882 883=head1 TODO 884 885It would be nice to make some of the structured sections smarter -- e.g. 886look at changed files in pod/* for Documentation section entries. Likewise 887it would be nice to collate them during the render phase -- e.g. cluster 888all platform-specific things properly. 889 890=head1 AUTHOR 891 892David Golden <dagolden@cpan.org> 893 894=head1 COPYRIGHT AND LICENSE 895 896This software is copyright (c) 2010 by David Golden. 897 898This is free software; you can redistribute it and/or modify it under the same 899terms as the Perl 5 programming language system itself. 900 901=cut 902 903