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