xref: /openbsd-src/gnu/usr.bin/perl/Porting/git-deltatool (revision 6fb12b7054efc6b436584db6cef9c2f85c0d7e27)
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