1898184e3Ssthen#!/usr/bin/perl 2898184e3Ssthen# 3898184e3Ssthen# This is a rough draft of a tool to aid in generating a perldelta file 4898184e3Ssthen# from a series of git commits. 5898184e3Ssthen 6898184e3Ssthenuse 5.010; 7898184e3Ssthenuse strict; 8898184e3Ssthenuse warnings; 9898184e3Ssthenpackage Git::DeltaTool; 10898184e3Ssthen 11898184e3Ssthenuse Class::Struct; 12898184e3Ssthenuse File::Basename; 13898184e3Ssthenuse File::Temp; 14898184e3Ssthenuse Getopt::Long; 15898184e3Ssthenuse Git::Wrapper; 16898184e3Ssthenuse Term::ReadKey; 17898184e3Ssthenuse Term::ANSIColor; 18898184e3Ssthenuse Pod::Usage; 19898184e3Ssthen 20898184e3SsthenBEGIN { struct( git => '$', last_tag => '$', opt => '%', original_stdout => '$' ) } 21898184e3Ssthen 22898184e3Ssthen__PACKAGE__->run; 23898184e3Ssthen 24898184e3Ssthen#--------------------------------------------------------------------------# 25898184e3Ssthen# main program 26898184e3Ssthen#--------------------------------------------------------------------------# 27898184e3Ssthen 28898184e3Ssthensub run { 29898184e3Ssthen my $class = shift; 30898184e3Ssthen 31898184e3Ssthen my %opt = ( 32898184e3Ssthen mode => 'assign', 33898184e3Ssthen ); 34898184e3Ssthen 35898184e3Ssthen GetOptions( \%opt, 36898184e3Ssthen # inputs 37898184e3Ssthen 'mode|m:s', # 'assign', 'review', 'render', 'update' 38898184e3Ssthen 'type|t:s', # select by status 39898184e3Ssthen 'status|s:s', # status to set for 'update' 40898184e3Ssthen 'since:s', # origin commit 41898184e3Ssthen 'help|h', # help 42898184e3Ssthen ); 43898184e3Ssthen 44898184e3Ssthen pod2usage() if $opt{help}; 45898184e3Ssthen 46898184e3Ssthen my $git = Git::Wrapper->new("."); 47898184e3Ssthen my $git_id = $opt{since}; 48898184e3Ssthen if ( defined $git_id ) { 49898184e3Ssthen die "Invalid git identifier '$git_id'\n" 50898184e3Ssthen unless eval { $git->show($git_id); 1 }; 51898184e3Ssthen } else { 52898184e3Ssthen ($git_id) = $git->describe; 53898184e3Ssthen $git_id =~ s/-.*$//; 54898184e3Ssthen } 55898184e3Ssthen my $gdt = $class->new( git => $git, last_tag => $git_id, opt => \%opt ); 56898184e3Ssthen 57898184e3Ssthen if ( $opt{mode} eq 'assign' ) { 58898184e3Ssthen $opt{type} //= 'new'; 59898184e3Ssthen $gdt->assign; 60898184e3Ssthen } 61898184e3Ssthen elsif ( $opt{mode} eq 'review' ) { 62898184e3Ssthen $opt{type} //= 'pending'; 63898184e3Ssthen $gdt->review; 64898184e3Ssthen } 65898184e3Ssthen elsif ( $opt{mode} eq 'render' ) { 66898184e3Ssthen $opt{type} //= 'pending'; 67898184e3Ssthen $gdt->render; 68898184e3Ssthen } 69898184e3Ssthen elsif ( $opt{mode} eq 'summary' ) { 70898184e3Ssthen $opt{type} //= 'pending'; 71898184e3Ssthen $gdt->summary; 72898184e3Ssthen } 73898184e3Ssthen elsif ( $opt{mode} eq 'update' ) { 74898184e3Ssthen die "Explicit --type argument required for update mode\n" 75898184e3Ssthen unless defined $opt{type}; 76898184e3Ssthen die "Explicit --status argument required for update mode\n" 77898184e3Ssthen unless defined $opt{status}; 78898184e3Ssthen $gdt->update; 79898184e3Ssthen } 80898184e3Ssthen else { 81898184e3Ssthen die "Unrecognized mode '$opt{mode}'\n"; 82898184e3Ssthen } 83898184e3Ssthen exit 0; 84898184e3Ssthen} 85898184e3Ssthen 86898184e3Ssthen#--------------------------------------------------------------------------# 87898184e3Ssthen# program modes (and iterator) 88898184e3Ssthen#--------------------------------------------------------------------------# 89898184e3Ssthen 90898184e3Ssthensub assign { 91898184e3Ssthen my ($self) = @_; 92898184e3Ssthen my @choices = ( $self->section_choices, $self->action_choices ); 93898184e3Ssthen $self->_iterate_commits( 94898184e3Ssthen sub { 95898184e3Ssthen my ($log, $i, $count) = @_; 96898184e3Ssthen say "\n### Commit @{[$i+1]} of $count ###"; 97898184e3Ssthen say "-" x 75; 98898184e3Ssthen $self->show_header($log); 99898184e3Ssthen $self->show_body($log, 1); 100*6fb12b70Safresh1 $self->show_files($log); 101898184e3Ssthen say "-" x 75; 102898184e3Ssthen return $self->dispatch( $self->prompt( @choices ), $log); 103898184e3Ssthen } 104898184e3Ssthen ); 105898184e3Ssthen return; 106898184e3Ssthen} 107898184e3Ssthen 108898184e3Ssthensub review { 109898184e3Ssthen my ($self) = @_; 110898184e3Ssthen my @choices = ( $self->review_choices, $self->action_choices ); 111898184e3Ssthen $self->_iterate_commits( 112898184e3Ssthen sub { 113898184e3Ssthen my ($log, $i, $count) = @_; 114898184e3Ssthen say "\n### Commit @{[$i+1]} of $count ###"; 115898184e3Ssthen say "-" x 75; 116898184e3Ssthen $self->show_header($log); 117898184e3Ssthen $self->show_notes($log, 1); 118898184e3Ssthen say "-" x 75; 119898184e3Ssthen return $self->dispatch( $self->prompt( @choices ), $log); 120898184e3Ssthen } 121898184e3Ssthen ); 122898184e3Ssthen return; 123898184e3Ssthen} 124898184e3Ssthen 125898184e3Ssthensub render { 126898184e3Ssthen my ($self) = @_; 127898184e3Ssthen my %sections; 128898184e3Ssthen $self->_iterate_commits( 129898184e3Ssthen sub { 130898184e3Ssthen my $log = shift; 131898184e3Ssthen my $section = $self->note_section($log) or return; 132898184e3Ssthen push @{ $sections{$section} }, $self->note_delta($log); 133898184e3Ssthen return 1; 134898184e3Ssthen } 135898184e3Ssthen ); 136898184e3Ssthen my @order = $self->section_order; 137898184e3Ssthen my %known = map { $_ => 1 } @order; 138898184e3Ssthen my @rest = grep { ! $known{$_} } keys %sections; 139898184e3Ssthen for my $s ( @order, @rest ) { 140898184e3Ssthen next unless ref $sections{$s}; 141898184e3Ssthen say "-"x75; 142898184e3Ssthen say uc($s) . "\n"; 143898184e3Ssthen say join ( "\n", @{ $sections{$s} }, "" ); 144898184e3Ssthen } 145898184e3Ssthen return; 146898184e3Ssthen} 147898184e3Ssthen 148898184e3Ssthensub summary { 149898184e3Ssthen my ($self) = @_; 150898184e3Ssthen $self->_iterate_commits( 151898184e3Ssthen sub { 152898184e3Ssthen my $log = shift; 153898184e3Ssthen $self->show_header($log); 154898184e3Ssthen return 1; 155898184e3Ssthen } 156898184e3Ssthen ); 157898184e3Ssthen return; 158898184e3Ssthen} 159898184e3Ssthen 160898184e3Ssthensub update { 161898184e3Ssthen my ($self) = @_; 162898184e3Ssthen 163898184e3Ssthen my $status = $self->opt('status') 164898184e3Ssthen or die "The 'status' option must be supplied for update mode\n"; 165898184e3Ssthen 166898184e3Ssthen $self->_iterate_commits( 167898184e3Ssthen sub { 168898184e3Ssthen my $log = shift; 169898184e3Ssthen my $note = $log->notes; 170898184e3Ssthen $note =~ s{^(perldelta.*\[)\w+(\].*)}{$1$status$2}ms; 171898184e3Ssthen $self->add_note( $log->id, $note ); 172898184e3Ssthen return 1; 173898184e3Ssthen } 174898184e3Ssthen ); 175898184e3Ssthen return; 176898184e3Ssthen} 177898184e3Ssthen 178898184e3Ssthensub _iterate_commits { 179898184e3Ssthen my ($self, $fcn) = @_; 180898184e3Ssthen my $type = $self->opt('type'); 181898184e3Ssthen say STDERR "Scanning for $type commits since " . $self->last_tag . "..."; 182898184e3Ssthen my $list = [ $self->find_commits($type) ]; 183898184e3Ssthen my $count = @$list; 184898184e3Ssthen while ( my ($i,$log) = each @$list ) { 185898184e3Ssthen redo unless $fcn->($log, $i, $count); 186898184e3Ssthen } 187898184e3Ssthen return 1; 188898184e3Ssthen} 189898184e3Ssthen 190898184e3Ssthen#--------------------------------------------------------------------------# 191898184e3Ssthen# methods 192898184e3Ssthen#--------------------------------------------------------------------------# 193898184e3Ssthen 194898184e3Ssthensub add_note { 195898184e3Ssthen my ($self, $id, $note) = @_; 196898184e3Ssthen my @lines = split "\n", _strip_comments($note); 197898184e3Ssthen pop @lines while @lines && $lines[-1] =~ m{^\s*$}; 198898184e3Ssthen my $tempfh = File::Temp->new; 199898184e3Ssthen if (@lines) { 200898184e3Ssthen $tempfh->printflush( join( "\n", @lines), "\n" ); 201898184e3Ssthen $self->git->notes('edit', '-F', "$tempfh", $id); 202898184e3Ssthen } 203898184e3Ssthen else { 204898184e3Ssthen $tempfh->printflush( "\n" ); 205898184e3Ssthen # git notes won't take an empty file as input 206898184e3Ssthen system("git notes edit -F $tempfh $id"); 207898184e3Ssthen } 208898184e3Ssthen 209898184e3Ssthen return; 210898184e3Ssthen} 211898184e3Ssthen 212898184e3Ssthensub dispatch { 213898184e3Ssthen my ($self, $choice, $log) = @_; 214898184e3Ssthen return unless $choice; 215898184e3Ssthen my $method = "do_$choice->{handler}"; 216898184e3Ssthen return 1 unless $self->can($method); # missing methods "succeed" 217898184e3Ssthen return $self->$method($choice, $log); 218898184e3Ssthen} 219898184e3Ssthen 220898184e3Ssthensub edit_text { 221898184e3Ssthen my ($self, $text, $args) = @_; 222898184e3Ssthen $args //= {}; 223898184e3Ssthen my $tempfh = File::Temp->new; 224898184e3Ssthen $tempfh->printflush( $text ); 225898184e3Ssthen if ( my @editor = split /\s+/, ($ENV{VISUAL} || $ENV{EDITOR}) ) { 226898184e3Ssthen push @editor, "-f" if $editor[0] =~ /^gvim/; 227898184e3Ssthen system(@editor, "$tempfh"); 228898184e3Ssthen } 229898184e3Ssthen else { 230898184e3Ssthen warn("No VISUAL or EDITOR defined"); 231898184e3Ssthen } 232*6fb12b70Safresh1 return do { local (@ARGV,$/) = "$tempfh"; <> }; 233898184e3Ssthen} 234898184e3Ssthen 235898184e3Ssthensub find_commits { 236898184e3Ssthen my ($self, $type) = @_; 237898184e3Ssthen $type //= 'new'; 238898184e3Ssthen my @commits = $self->git->log($self->last_tag . "..HEAD"); 239*6fb12b70Safresh1 $_ = Git::Wrapper::XLog->from_log($_, $self->git) for @commits; 240898184e3Ssthen my @list; 241898184e3Ssthen if ( $type eq 'new' ) { 242898184e3Ssthen @list = grep { ! $_->notes } @commits; 243898184e3Ssthen } 244898184e3Ssthen else { 245898184e3Ssthen @list = grep { $self->note_status( $_ ) eq $type } @commits; 246898184e3Ssthen } 247898184e3Ssthen return @list; 248898184e3Ssthen} 249898184e3Ssthen 250898184e3Ssthensub get_diff { 251898184e3Ssthen my ($self, $log) = @_; 252898184e3Ssthen my @diff = $self->git->show({ stat => 1, p => 1 }, $log->id); 253898184e3Ssthen return join("\n", @diff); 254898184e3Ssthen} 255898184e3Ssthen 256898184e3Ssthensub note_delta { 257898184e3Ssthen my ($self, $log) = @_; 258898184e3Ssthen my @delta = split "\n", ($log->notes || ''); 259898184e3Ssthen return '' unless @delta; 260898184e3Ssthen splice @delta, 0, 2; 261898184e3Ssthen return join( "\n", @delta, "" ); 262898184e3Ssthen} 263898184e3Ssthen 264898184e3Ssthensub note_section { 265898184e3Ssthen my ($self, $log) = @_; 266898184e3Ssthen my $note = $log->notes or return ''; 267898184e3Ssthen my ($section) = $note =~ m{^perldelta:\s*([^\[]*)\s+}ms; 268898184e3Ssthen return $section || ''; 269898184e3Ssthen} 270898184e3Ssthen 271898184e3Ssthensub note_status { 272898184e3Ssthen my ($self, $log) = @_; 273898184e3Ssthen my $note = $log->notes or return ''; 274898184e3Ssthen my ($status) = $note =~ m{^perldelta:\s*[^\[]*\[(\w+)\]}ms; 275898184e3Ssthen return $status || ''; 276898184e3Ssthen} 277898184e3Ssthen 278898184e3Ssthensub note_template { 279898184e3Ssthen my ($self, $log, $text) = @_; 280898184e3Ssthen my $diff = _prepend_comment( $self->get_diff($log) ); 281898184e3Ssthen return << "HERE"; 282898184e3Ssthen# Edit commit note below. Do not change the first line. Comments are stripped 283898184e3Ssthen$text 284898184e3Ssthen 285898184e3Ssthen$diff 286898184e3SsthenHERE 287898184e3Ssthen} 288898184e3Ssthen 289898184e3Ssthensub prompt { 290898184e3Ssthen my ($self, @choices) = @_; 291898184e3Ssthen my ($valid, @menu, %keymap) = ''; 292898184e3Ssthen for my $c ( map { @$_ } @choices ) { 293898184e3Ssthen my ($item) = grep { /\(/ } split q{ }, $c->{name}; 294898184e3Ssthen my ($button) = $item =~ m{\((.)\)}; 295898184e3Ssthen die "No key shortcut found for '$item'" unless $button; 296898184e3Ssthen die "Duplicate key shortcut found for '$item'" if $keymap{lc $button}; 297898184e3Ssthen push @menu, $item; 298898184e3Ssthen $valid .= lc $button; 299898184e3Ssthen $keymap{lc $button} = $c; 300898184e3Ssthen } 301898184e3Ssthen my $keypress = $self->prompt_key( $self->wrap_list(@menu), $valid ); 302898184e3Ssthen return $keymap{lc $keypress}; 303898184e3Ssthen} 304898184e3Ssthen 305898184e3Ssthensub prompt_key { 306898184e3Ssthen my ($self, $prompt, $valid_keys) = @_; 307898184e3Ssthen my $key; 308898184e3Ssthen KEY: { 309898184e3Ssthen say $prompt; 310898184e3Ssthen ReadMode 3; 311898184e3Ssthen $key = lc ReadKey(0); 312898184e3Ssthen ReadMode 0; 313898184e3Ssthen if ( $key !~ qr/\A[$valid_keys]\z/i ) { 314898184e3Ssthen say ""; 315898184e3Ssthen redo KEY; 316898184e3Ssthen } 317898184e3Ssthen } 318898184e3Ssthen return $key; 319898184e3Ssthen} 320898184e3Ssthen 321898184e3Ssthensub show_body { 322898184e3Ssthen my ($self, $log, $lf) = @_; 323898184e3Ssthen return unless my $body = $log->body; 324898184e3Ssthen say $lf ? "\n$body" : $body; 325898184e3Ssthen return; 326898184e3Ssthen} 327898184e3Ssthen 328*6fb12b70Safresh1sub show_files { 329*6fb12b70Safresh1 my ($self, $log) = @_; 330*6fb12b70Safresh1 my @files = $self->git->diff_tree({r => 1, abbrev => 1}, $log->id); 331*6fb12b70Safresh1 shift @files; # throw away commit line 332*6fb12b70Safresh1 return unless @files; 333*6fb12b70Safresh1 say "\nChanged:"; 334*6fb12b70Safresh1 say join("\n", map { " * $_" } sort map { /.*\s+(\S+)/; $1 } @files); 335*6fb12b70Safresh1 return; 336*6fb12b70Safresh1} 337*6fb12b70Safresh1 338898184e3Ssthensub show_header { 339898184e3Ssthen my ($self, $log) = @_; 340898184e3Ssthen my $header = $log->short_id; 341898184e3Ssthen $header .= " " . $log->subject if length $log->subject; 342898184e3Ssthen $header .= sprintf(' (%s)', $log->author) if $log->author; 343898184e3Ssthen say colored( $header, "yellow"); 344898184e3Ssthen return; 345898184e3Ssthen} 346898184e3Ssthen 347898184e3Ssthensub show_notes { 348898184e3Ssthen my ($self, $log, $lf) = @_; 349898184e3Ssthen return unless my $notes = $log->notes; 350898184e3Ssthen say $lf ? "\n$notes" : $notes; 351898184e3Ssthen return; 352898184e3Ssthen} 353898184e3Ssthen 354898184e3Ssthensub wrap_list { 355898184e3Ssthen my ($self, @list) = @_; 356898184e3Ssthen my $line = shift @list; 357898184e3Ssthen my @wrap; 358898184e3Ssthen for my $item ( @list ) { 359898184e3Ssthen if ( length( $line . $item ) > 70 ) { 360898184e3Ssthen push @wrap, $line; 361898184e3Ssthen $line = $item ne $list[-1] ? $item : "or $item"; 362898184e3Ssthen } 363898184e3Ssthen else { 364898184e3Ssthen $line .= $item ne $list[-1] ? ", $item" : " or $item"; 365898184e3Ssthen } 366898184e3Ssthen } 367898184e3Ssthen return join("\n", @wrap, $line); 368898184e3Ssthen} 369898184e3Ssthen 370898184e3Ssthensub y_n { 371898184e3Ssthen my ($self, $msg) = @_; 372898184e3Ssthen my $key = $self->prompt_key($msg . " (y/n?)", 'yn'); 373898184e3Ssthen return $key eq 'y'; 374898184e3Ssthen} 375898184e3Ssthen 376898184e3Ssthen#--------------------------------------------------------------------------# 377898184e3Ssthen# handlers 378898184e3Ssthen#--------------------------------------------------------------------------# 379898184e3Ssthen 380898184e3Ssthensub do_blocking { 381898184e3Ssthen my ($self, $choice, $log) = @_; 382898184e3Ssthen my $note = "perldelta: Unknown [blocking]\n"; 383898184e3Ssthen $self->add_note( $log->id, $note ); 384898184e3Ssthen return 1; 385898184e3Ssthen} 386898184e3Ssthen 387898184e3Ssthensub do_examine { 388898184e3Ssthen my ($self, $choice, $log) = @_; 389898184e3Ssthen $self->start_pager; 390898184e3Ssthen say $self->get_diff($log); 391898184e3Ssthen $self->end_pager; 392898184e3Ssthen return; 393898184e3Ssthen} 394898184e3Ssthen 395898184e3Ssthensub do_cherry { 396898184e3Ssthen my ($self, $choice, $log) = @_; 397898184e3Ssthen my $id = $log->short_id; 398898184e3Ssthen $self->y_n("Recommend a cherry pick of '$id' to maint?") or return; 399898184e3Ssthen my $cherrymaint = dirname($0) . "/cherrymaint"; 400898184e3Ssthen system("$^X $cherrymaint --vote $id"); 401898184e3Ssthen return; # false will re-prompt the same commit 402898184e3Ssthen} 403898184e3Ssthen 404898184e3Ssthensub do_done { 405898184e3Ssthen my ($self, $choice, $log) = @_; 406898184e3Ssthen my $note = $log->notes; 407898184e3Ssthen $note =~ s{^(perldelta.*\[)\w+(\].*)}{$1done$2}ms; 408898184e3Ssthen $self->add_note( $log->id, $note ); 409898184e3Ssthen return 1; 410898184e3Ssthen} 411898184e3Ssthen 412898184e3Ssthensub do_edit { 413898184e3Ssthen my ($self, $choice, $log) = @_; 414898184e3Ssthen my $old_note = $log->notes; 415898184e3Ssthen my $new_note = $self->edit_text( $self->note_template( $log, $old_note) ); 416898184e3Ssthen $self->add_note( $log->id, $new_note ); 417898184e3Ssthen return 1; 418898184e3Ssthen} 419898184e3Ssthen 420898184e3Ssthensub do_head2 { 421898184e3Ssthen my ($self, $choice, $log) = @_; 422898184e3Ssthen my $section = _strip_parens($choice->{name}); 423898184e3Ssthen my $subject = $log->subject; 424898184e3Ssthen my $body = $log->body; 425898184e3Ssthen 426898184e3Ssthen my $template = $self->note_template( $log, 427898184e3Ssthen "perldelta: $section [pending]\n\n=head2 $subject\n\n$body\n" 428898184e3Ssthen ); 429898184e3Ssthen 430898184e3Ssthen my $note = $self->edit_text( $template ); 431898184e3Ssthen if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 432898184e3Ssthen $self->add_note( $log->id, $note ); 433898184e3Ssthen return 1; 434898184e3Ssthen } 435898184e3Ssthen return; 436898184e3Ssthen} 437898184e3Ssthen 438898184e3Ssthensub do_linked_item { 439898184e3Ssthen my ($self, $choice, $log) = @_; 440898184e3Ssthen my $section = _strip_parens($choice->{name}); 441898184e3Ssthen my $subject = $log->subject; 442898184e3Ssthen my $body = $log->body; 443898184e3Ssthen 444898184e3Ssthen my $template = $self->note_template( $log, 445898184e3Ssthen "perldelta: $section [pending]\n\n=head3 L<LINK>\n\n=over\n\n=item *\n\n$subject\n\n$body\n\n=back\n" 446898184e3Ssthen ); 447898184e3Ssthen 448898184e3Ssthen my $note = $self->edit_text($template); 449898184e3Ssthen if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 450898184e3Ssthen $self->add_note( $log->id, $note ); 451898184e3Ssthen return 1; 452898184e3Ssthen } 453898184e3Ssthen return; 454898184e3Ssthen} 455898184e3Ssthen 456898184e3Ssthensub do_item { 457898184e3Ssthen my ($self, $choice, $log) = @_; 458898184e3Ssthen my $section = _strip_parens($choice->{name}); 459898184e3Ssthen my $subject = $log->subject; 460898184e3Ssthen my $body = $log->body; 461898184e3Ssthen 462898184e3Ssthen my $template = $self->note_template( $log, 463898184e3Ssthen "perldelta: $section [pending]\n\n=item *\n\n$subject\n\n$body\n" 464898184e3Ssthen ); 465898184e3Ssthen 466898184e3Ssthen my $note = $self->edit_text($template); 467898184e3Ssthen if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 468898184e3Ssthen $self->add_note( $log->id, $note ); 469898184e3Ssthen return 1; 470898184e3Ssthen } 471898184e3Ssthen return; 472898184e3Ssthen} 473898184e3Ssthen 474898184e3Ssthensub do_none { 475898184e3Ssthen my ($self, $choice, $log) = @_; 476898184e3Ssthen my $note = "perldelta: None [ignored]\n"; 477898184e3Ssthen $self->add_note( $log->id, $note ); 478898184e3Ssthen return 1; 479898184e3Ssthen} 480898184e3Ssthen 481898184e3Ssthensub do_platform { 482898184e3Ssthen my ($self, $choice, $log) = @_; 483898184e3Ssthen my $section = _strip_parens($choice->{name}); 484898184e3Ssthen my $subject = $log->subject; 485898184e3Ssthen my $body = $log->body; 486898184e3Ssthen 487898184e3Ssthen my $template = $self->note_template( $log, 488898184e3Ssthen "perldelta: $section [pending]\n\n=item PLATFORM-NAME\n\n$subject\n\n$body\n" 489898184e3Ssthen ); 490898184e3Ssthen 491898184e3Ssthen my $note = $self->edit_text($template); 492898184e3Ssthen if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 493898184e3Ssthen $self->add_note( $log->id, $note ); 494898184e3Ssthen return 1; 495898184e3Ssthen } 496898184e3Ssthen return; 497898184e3Ssthen} 498898184e3Ssthen 499898184e3Ssthensub do_quit { exit 0 } 500898184e3Ssthen 501898184e3Ssthensub do_repeat { return 0 } 502898184e3Ssthen 503898184e3Ssthensub do_skip { return 1 } 504898184e3Ssthen 505898184e3Ssthensub do_special { 506898184e3Ssthen my ($self, $choice, $log) = @_; 507898184e3Ssthen my $section = _strip_parens($choice->{name}); 508898184e3Ssthen my $subject = $log->subject; 509898184e3Ssthen my $body = $log->body; 510898184e3Ssthen 511898184e3Ssthen my $template = $self->note_template( $log, << "HERE" ); 512898184e3Ssthenperldelta: $section [pending] 513898184e3Ssthen 514898184e3Ssthen$subject 515898184e3Ssthen 516898184e3Ssthen$body 517898184e3SsthenHERE 518898184e3Ssthen 519898184e3Ssthen my $note = $self->edit_text( $template ); 520898184e3Ssthen if ( ($note ne $template) or $self->y_n("Note unchanged. Commit it?") ) { 521898184e3Ssthen $self->add_note( $log->id, $note ); 522898184e3Ssthen return 1; 523898184e3Ssthen } 524898184e3Ssthen return; 525898184e3Ssthen} 526898184e3Ssthen 527898184e3Ssthensub do_subsection { 528898184e3Ssthen my ($self, $choice, $log) = @_; 529898184e3Ssthen my @choices = ( $choice->{subsection}, $self->submenu_choices ); 530898184e3Ssthen say "For " . _strip_parens($choice->{name}) . ":"; 531898184e3Ssthen return $self->dispatch( $self->prompt( @choices ), $log); 532898184e3Ssthen} 533898184e3Ssthen 534898184e3Ssthen#--------------------------------------------------------------------------# 535898184e3Ssthen# define prompts 536898184e3Ssthen#--------------------------------------------------------------------------# 537898184e3Ssthen 538898184e3Ssthensub action_choices { 539898184e3Ssthen my ($self) = @_; 540898184e3Ssthen state $action_choices = [ 541898184e3Ssthen { name => 'E(x)amine', handler => 'examine' }, 542898184e3Ssthen { name => '(+)Cherrymaint', handler => 'cherry' }, 543898184e3Ssthen { name => '(?)NeedHelp', handler => 'blocking' }, 544898184e3Ssthen { name => 'S(k)ip', handler => 'skip' }, 545898184e3Ssthen { name => '(Q)uit', handler => 'quit' }, 546898184e3Ssthen ]; 547898184e3Ssthen return $action_choices; 548898184e3Ssthen} 549898184e3Ssthen 550898184e3Ssthensub submenu_choices { 551898184e3Ssthen my ($self) = @_; 552898184e3Ssthen state $submenu_choices = [ 553898184e3Ssthen { name => '(B)ack', handler => 'repeat' }, 554898184e3Ssthen ]; 555898184e3Ssthen return $submenu_choices; 556898184e3Ssthen} 557898184e3Ssthen 558898184e3Ssthen 559898184e3Ssthensub review_choices { 560898184e3Ssthen my ($self) = @_; 561898184e3Ssthen state $action_choices = [ 562898184e3Ssthen { name => '(E)dit', handler => 'edit' }, 563898184e3Ssthen { name => '(I)gnore', handler => 'none' }, 564898184e3Ssthen { name => '(D)one', handler => 'done' }, 565898184e3Ssthen ]; 566898184e3Ssthen return $action_choices; 567898184e3Ssthen} 568898184e3Ssthen 569898184e3Ssthensub section_choices { 570898184e3Ssthen my ($self, $key) = @_; 571898184e3Ssthen state $section_choices = [ 572898184e3Ssthen # Headline stuff that should go first 573898184e3Ssthen { 574898184e3Ssthen name => 'Core (E)nhancements', 575898184e3Ssthen handler => 'head2', 576898184e3Ssthen }, 577898184e3Ssthen { 578898184e3Ssthen name => 'Securit(y)', 579898184e3Ssthen handler => 'head2', 580898184e3Ssthen }, 581898184e3Ssthen { 582898184e3Ssthen name => '(I)ncompatible Changes', 583898184e3Ssthen handler => 'head2', 584898184e3Ssthen }, 585898184e3Ssthen { 586898184e3Ssthen name => 'Dep(r)ecations', 587898184e3Ssthen handler => 'head2', 588898184e3Ssthen }, 589898184e3Ssthen { 590898184e3Ssthen name => '(P)erformance Enhancements', 591898184e3Ssthen handler => 'item', 592898184e3Ssthen }, 593898184e3Ssthen 594898184e3Ssthen # Details on things installed with Perl (for Perl developers) 595898184e3Ssthen { 596898184e3Ssthen name => '(M)odules and Pragmata', 597898184e3Ssthen handler => 'subsection', 598898184e3Ssthen subsection => [ 599898184e3Ssthen { 600898184e3Ssthen name => '(N)ew Modules and Pragmata', 601898184e3Ssthen handler => 'item', 602898184e3Ssthen }, 603898184e3Ssthen { 604898184e3Ssthen name => '(U)pdated Modules and Pragmata', 605898184e3Ssthen handler => 'item', 606898184e3Ssthen }, 607898184e3Ssthen { 608898184e3Ssthen name => '(R)emoved Modules and Pragmata', 609898184e3Ssthen handler => 'item', 610898184e3Ssthen }, 611898184e3Ssthen ], 612898184e3Ssthen }, 613898184e3Ssthen { 614898184e3Ssthen name => '(D)ocumentation', 615898184e3Ssthen handler => 'subsection', 616898184e3Ssthen subsection => [ 617898184e3Ssthen { 618898184e3Ssthen name => '(N)ew Documentation', 619898184e3Ssthen handler => 'linked_item', 620898184e3Ssthen }, 621898184e3Ssthen { 622898184e3Ssthen name => '(C)hanges to Existing Documentation', 623898184e3Ssthen handler => 'linked_item', 624898184e3Ssthen }, 625898184e3Ssthen ], 626898184e3Ssthen }, 627898184e3Ssthen { 628898184e3Ssthen name => 'Dia(g)nostics', 629898184e3Ssthen handler => 'subsection', 630898184e3Ssthen subsection => [ 631898184e3Ssthen { 632898184e3Ssthen name => '(N)ew Diagnostics', 633898184e3Ssthen handler => 'item', 634898184e3Ssthen }, 635898184e3Ssthen { 636898184e3Ssthen name => '(C)hanges to Existing Diagnostics', 637898184e3Ssthen handler => 'item', 638898184e3Ssthen }, 639898184e3Ssthen ], 640898184e3Ssthen }, 641898184e3Ssthen { 642898184e3Ssthen name => '(U)tilities', 643898184e3Ssthen handler => 'linked_item', 644898184e3Ssthen }, 645898184e3Ssthen 646898184e3Ssthen # Details on building/testing Perl (for porters and packagers) 647898184e3Ssthen { 648898184e3Ssthen name => '(C)onfiguration and Compilation', 649898184e3Ssthen handler => 'item', 650898184e3Ssthen }, 651898184e3Ssthen { 652898184e3Ssthen name => '(T)esting', # new tests or significant notes about it 653898184e3Ssthen handler => 'item', 654898184e3Ssthen }, 655898184e3Ssthen { 656898184e3Ssthen name => 'Pl(a)tform Support', 657898184e3Ssthen handler => 'subsection', 658898184e3Ssthen subsection => [ 659898184e3Ssthen { 660898184e3Ssthen name => '(N)ew Platforms', 661898184e3Ssthen handler => 'platform', 662898184e3Ssthen }, 663898184e3Ssthen { 664898184e3Ssthen name => '(D)iscontinued Platforms', 665898184e3Ssthen handler => 'platform', 666898184e3Ssthen }, 667898184e3Ssthen { 668898184e3Ssthen name => '(P)latform-Specific Notes', 669898184e3Ssthen handler => 'platform', 670898184e3Ssthen }, 671898184e3Ssthen ], 672898184e3Ssthen }, 673898184e3Ssthen 674898184e3Ssthen # Details on perl internals (for porters and XS developers) 675898184e3Ssthen { 676898184e3Ssthen name => 'Inter(n)al Changes', 677898184e3Ssthen handler => 'item', 678898184e3Ssthen }, 679898184e3Ssthen 680898184e3Ssthen # Bugs fixed and related stuff 681898184e3Ssthen { 682898184e3Ssthen name => 'Selected Bug (F)ixes', 683898184e3Ssthen handler => 'item', 684898184e3Ssthen }, 685898184e3Ssthen { 686898184e3Ssthen name => 'Known Prob(l)ems', 687898184e3Ssthen handler => 'item', 688898184e3Ssthen }, 689898184e3Ssthen 690898184e3Ssthen # dummy options for special handling 691898184e3Ssthen { 692898184e3Ssthen name => '(S)pecial', 693898184e3Ssthen handler => 'special', 694898184e3Ssthen }, 695898184e3Ssthen { 696898184e3Ssthen name => '(*)None', 697898184e3Ssthen handler => 'none', 698898184e3Ssthen }, 699898184e3Ssthen ]; 700898184e3Ssthen return $section_choices; 701898184e3Ssthen} 702898184e3Ssthen 703898184e3Ssthensub section_order { 704898184e3Ssthen my ($self) = @_; 705898184e3Ssthen state @order; 706898184e3Ssthen if ( ! @order ) { 707898184e3Ssthen for my $c ( @{ $self->section_choices } ) { 708898184e3Ssthen if ( $c->{subsection} ) { 709898184e3Ssthen push @order, map { $_->{name} } @{$c->{subsection}}; 710898184e3Ssthen } 711898184e3Ssthen else { 712898184e3Ssthen push @order, $c->{name}; 713898184e3Ssthen } 714898184e3Ssthen } 715898184e3Ssthen } 716898184e3Ssthen return @order; 717898184e3Ssthen} 718898184e3Ssthen 719898184e3Ssthen#--------------------------------------------------------------------------# 720898184e3Ssthen# Pager handling 721898184e3Ssthen#--------------------------------------------------------------------------# 722898184e3Ssthen 723898184e3Ssthensub get_pager { $ENV{'PAGER'} || `which less` || `which more` } 724898184e3Ssthen 725898184e3Ssthensub in_pager { shift->original_stdout ? 1 : 0 } 726898184e3Ssthen 727898184e3Ssthensub start_pager { 728898184e3Ssthen my $self = shift; 729898184e3Ssthen my $content = shift; 730898184e3Ssthen if (!$self->in_pager) { 731898184e3Ssthen local $ENV{'LESS'} ||= '-FXe'; 732898184e3Ssthen local $ENV{'MORE'}; 733898184e3Ssthen $ENV{'MORE'} ||= '-FXe' unless $^O =~ /^MSWin/; 734898184e3Ssthen 735898184e3Ssthen my $pager = $self->get_pager; 736898184e3Ssthen return unless $pager; 737898184e3Ssthen open (my $cmd, "|-", $pager) || return; 738898184e3Ssthen $|++; 739898184e3Ssthen $self->original_stdout(*STDOUT); 740898184e3Ssthen 741898184e3Ssthen # $pager will be closed once we restore STDOUT to $original_stdout 742898184e3Ssthen *STDOUT = $cmd; 743898184e3Ssthen } 744898184e3Ssthen} 745898184e3Ssthen 746898184e3Ssthensub end_pager { 747898184e3Ssthen my $self = shift; 748898184e3Ssthen return unless ($self->in_pager); 749898184e3Ssthen *STDOUT = $self->original_stdout; 750898184e3Ssthen 751898184e3Ssthen # closes the pager 752898184e3Ssthen $self->original_stdout(undef); 753898184e3Ssthen} 754898184e3Ssthen 755898184e3Ssthen#--------------------------------------------------------------------------# 756898184e3Ssthen# Utility functions 757898184e3Ssthen#--------------------------------------------------------------------------# 758898184e3Ssthen 759898184e3Ssthensub _strip_parens { 760898184e3Ssthen my ($name) = @_; 761898184e3Ssthen $name =~ s/[()]//g; 762898184e3Ssthen return $name; 763898184e3Ssthen} 764898184e3Ssthen 765898184e3Ssthensub _prepend_comment { 766898184e3Ssthen my ($text) = @_; 767898184e3Ssthen return join ("\n", map { s/^/# /g; $_ } split "\n", $text); 768898184e3Ssthen} 769898184e3Ssthen 770898184e3Ssthensub _strip_comments { 771898184e3Ssthen my ($text) = @_; 772898184e3Ssthen return join ("\n", grep { ! /^#/ } split "\n", $text); 773898184e3Ssthen} 774898184e3Ssthen 775898184e3Ssthen#--------------------------------------------------------------------------# 776898184e3Ssthen# Extend Git::Wrapper::Log 777898184e3Ssthen#--------------------------------------------------------------------------# 778898184e3Ssthen 779898184e3Ssthenpackage Git::Wrapper::XLog; 780898184e3SsthenBEGIN { our @ISA = qw/Git::Wrapper::Log/; } 781898184e3Ssthen 782898184e3Ssthensub subject { shift->attr->{subject} } 783898184e3Ssthensub body { shift->attr->{body} } 784898184e3Ssthensub short_id { shift->attr->{short_id} } 785898184e3Ssthensub author { shift->attr->{author} } 786898184e3Ssthen 787898184e3Ssthensub from_log { 788*6fb12b70Safresh1 my ($class, $log, $git) = @_; 789898184e3Ssthen 790898184e3Ssthen my $msg = $log->message; 791898184e3Ssthen my ($subject, $body) = $msg =~ m{^([^\n]+)\n*(.*)}ms; 792898184e3Ssthen $subject //= ''; 793898184e3Ssthen $body //= ''; 794898184e3Ssthen $body =~ s/[\r\n]*\z//ms; 795898184e3Ssthen 796*6fb12b70Safresh1 my ($short) = $git->rev_parse({short => 1}, $log->id); 797898184e3Ssthen 798898184e3Ssthen $log->attr->{subject} = $subject; 799898184e3Ssthen $log->attr->{body} = $body; 800898184e3Ssthen $log->attr->{short_id} = $short; 801898184e3Ssthen return bless $log, $class; 802898184e3Ssthen} 803898184e3Ssthen 804898184e3Ssthensub notes { 805898184e3Ssthen my ($self) = @_; 806898184e3Ssthen my @notes = eval { Git::Wrapper->new(".")->notes('show', $self->id) }; 807898184e3Ssthen pop @notes while @notes && $notes[-1] =~ m{^\s*$}; 808898184e3Ssthen return unless @notes; 809898184e3Ssthen return join ("\n", @notes); 810898184e3Ssthen} 811898184e3Ssthen 812898184e3Ssthen__END__ 813898184e3Ssthen 814898184e3Ssthen=head1 NAME 815898184e3Ssthen 816898184e3Ssthengit-deltatool - Annotate commits for perldelta 817898184e3Ssthen 818898184e3Ssthen=head1 SYNOPSIS 819898184e3Ssthen 820898184e3Ssthen # annotate commits back to last 'git describe' tag 821898184e3Ssthen 822898184e3Ssthen $ git-deltatool 823898184e3Ssthen 824898184e3Ssthen # review annotations 825898184e3Ssthen 826898184e3Ssthen $ git-deltatool --mode review 827898184e3Ssthen 828898184e3Ssthen # review commits needing help 829898184e3Ssthen 830898184e3Ssthen $ git-deltatool --mode review --type blocking 831898184e3Ssthen 832898184e3Ssthen # summarize commits needing help 833898184e3Ssthen 834898184e3Ssthen $ git-deltatool --mode summary --type blocking 835898184e3Ssthen 836898184e3Ssthen # assemble annotations by section to STDOUT 837898184e3Ssthen 838898184e3Ssthen $ git-deltatool --mode render 839898184e3Ssthen 840898184e3Ssthen # Get a list of commits needing further review, e.g. for peer review 841898184e3Ssthen 842898184e3Ssthen $ git-deltatool --mode summary --type blocking 843898184e3Ssthen 844898184e3Ssthen # mark 'pending' annotations as 'done' (i.e. added to perldelta) 845898184e3Ssthen 846898184e3Ssthen $ git-deltatool --mode update --type pending --status done 847898184e3Ssthen 848898184e3Ssthen=head1 OPTIONS 849898184e3Ssthen 850898184e3Ssthen=over 851898184e3Ssthen 852898184e3Ssthen=item B<--mode>|B<-m> MODE 853898184e3Ssthen 854898184e3SsthenIndicates the run mode for the program. The default is 'assign' which 855898184e3Ssthenassigns categories and marks the notes as 'pending' (or 'ignored'). Other 856898184e3Ssthenmodes are 'review', 'render', 'summary' and 'update'. 857898184e3Ssthen 858898184e3Ssthen=item B<--type>|B<-t> TYPE 859898184e3Ssthen 860898184e3SsthenIndicates what types of commits to process. The default for 'assign' mode is 861898184e3Ssthen'new', which processes commits without any perldelta notes. The default for 862898184e3Ssthen'review', 'summary' and 'render' modes is 'pending'. The options must be set 863898184e3Ssthenexplicitly for 'update' mode. 864898184e3Ssthen 865898184e3SsthenThe type 'blocking' is reserved for commits needing further review. 866898184e3Ssthen 867898184e3Ssthen=item B<--status>|B<-s> STATUS 868898184e3Ssthen 869898184e3SsthenFor 'update' mode only, sets a new status. While there is no restriction, 870898184e3Ssthenit should be one of 'new', 'pending', 'blocking', 'ignored' or 'done'. 871898184e3Ssthen 872898184e3Ssthen=item B<--since> REVISION 873898184e3Ssthen 874898184e3SsthenDefines the boundary for searching git commits. Defaults to the last 875898184e3Ssthenmajor tag (as would be given by 'git describe'). 876898184e3Ssthen 877898184e3Ssthen=item B<--help> 878898184e3Ssthen 879898184e3SsthenShows the manual. 880898184e3Ssthen 881898184e3Ssthen=back 882898184e3Ssthen 883898184e3Ssthen=head1 TODO 884898184e3Ssthen 885898184e3SsthenIt would be nice to make some of the structured sections smarter -- e.g. 886898184e3Ssthenlook at changed files in pod/* for Documentation section entries. Likewise 887898184e3Ssthenit would be nice to collate them during the render phase -- e.g. cluster 888898184e3Ssthenall platform-specific things properly. 889898184e3Ssthen 890898184e3Ssthen=head1 AUTHOR 891898184e3Ssthen 892898184e3SsthenDavid Golden <dagolden@cpan.org> 893898184e3Ssthen 894898184e3Ssthen=head1 COPYRIGHT AND LICENSE 895898184e3Ssthen 896898184e3SsthenThis software is copyright (c) 2010 by David Golden. 897898184e3Ssthen 898898184e3SsthenThis is free software; you can redistribute it and/or modify it under the same 899898184e3Ssthenterms as the Perl 5 programming language system itself. 900898184e3Ssthen 901898184e3Ssthen=cut 902898184e3Ssthen 903