File Coverage

blib/lib/Dist/Zilla/Plugin/NextVersion/Semantic.pm
Criterion Covered Total %
statement 42 134 31.3
branch 0 22 0.0
condition 0 4 0.0
subroutine 14 26 53.8
pod 0 3 0.0
total 56 189 29.6


line stmt bran cond sub pod time code
1             package Dist::Zilla::Plugin::NextVersion::Semantic;
2             our $AUTHORITY = 'cpan:YANICK';
3             # ABSTRACT: update the next version, semantic-wise
4             $Dist::Zilla::Plugin::NextVersion::Semantic::VERSION = '0.2.5';
5 1     1   15638 use strict;
  1         2  
  1         35  
6 1     1   4 use warnings;
  1         0  
  1         24  
7              
8 1     1   10 use 5.10.0;
  1         3  
  1         58  
9              
10 1     1   463 use CPAN::Changes 0.20;
  1         29978  
  1         34  
11 1     1   513 use Perl::Version;
  1         3973  
  1         45  
12 1     1   840 use List::AllUtils qw/ any min /;
  1         12934  
  1         105  
13              
14 1     1   643 use Moose;
  1         395103  
  1         9  
15              
16             with qw/
17             Dist::Zilla::Role::Plugin
18             Dist::Zilla::Role::FileMunger
19             Dist::Zilla::Role::TextTemplate
20             Dist::Zilla::Role::BeforeRelease
21             Dist::Zilla::Role::AfterRelease
22             Dist::Zilla::Role::VersionProvider
23             Dist::Zilla::Plugin::NextVersion::Semantic::Incrementer
24             /;
25              
26 1     1   5549 use Moose::Util::TypeConstraints;
  1         3  
  1         11  
27              
28             subtype 'ChangeCategory',
29             as 'ArrayRef[Str]';
30              
31             coerce ChangeCategory =>
32             from 'Str',
33             via {
34             [ split /\s*,\s*/, $_ ]
35             };
36              
37              
38             has change_file => ( is => 'ro', isa=>'Str', default => 'Changes' );
39              
40              
41             has numify_version => ( is => 'ro', isa => 'Bool', default => 0 );
42              
43              
44             has format => (
45             is => 'ro',
46             isa => 'Str',
47             default => '%d.%3d.%3d',
48             );
49              
50              
51             has major => (
52             is => 'ro',
53             isa => 'ChangeCategory',
54             coerce => 1,
55             default => sub { [ 'API CHANGES' ] },
56             traits => ['Array'],
57             handles => { major_groups => 'elements' },
58             );
59              
60              
61             has minor => (
62             is => 'ro',
63             isa => 'ChangeCategory',
64             coerce => 1,
65             default => sub { [ 'ENHANCEMENTS', 'UNGROUPED' ] },
66             traits => ['Array'],
67             handles => { minor_groups => 'elements' },
68             );
69              
70              
71             has revision => (
72             is => 'ro',
73             isa => 'ChangeCategory',
74             coerce => 1,
75             default => sub { [ 'BUG FIXES', 'DOCUMENTATION' ] },
76             traits => ['Array'],
77             handles => { revision_groups => 'elements' },
78             );
79              
80             sub before_release {
81 0     0     my $self = shift;
82              
83 0           my ($changes_file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files };
  0            
  0            
84              
85 0           my $changes = CPAN::Changes->load_string(
86             $changes_file->content,
87             next_token => qr/\{\{\$NEXT\}\}/
88             );
89              
90 0           my( $next ) = reverse $changes->releases;
91              
92 0           my @changes = values %{ $next->changes };
  0            
93              
94 0 0         $self->log_fatal("change file has no content for next version")
95             unless @changes;
96              
97             }
98              
99             sub after_release {
100 0     0     my ($self) = @_;
101 0           my $filename = $self->change_file;
102              
103 0           my $changes = CPAN::Changes->load(
104             $self->change_file,
105             next_token => qr/\{\{\$NEXT\}\}/
106             );
107              
108             # remove empty groups
109 0           $changes->delete_empty_groups;
110              
111 0           my ( $next ) = reverse $changes->releases;
112              
113 0           $next->add_group( grep { $_ ne 'UNGROUPED' } $self->all_groups );
  0            
114              
115 0           $self->log_debug([ 'updating contents of %s on disk', $filename ]);
116              
117             # and finally rewrite the changelog on disk
118 0 0         open my $out_fh, '>', $filename
119             or Carp::croak("can't open $filename for writing: $!");
120              
121 0           print $out_fh $changes->serialize;
122              
123 0 0         close $out_fh or Carp::croak("error closing $filename: $!");
124             }
125              
126             sub all_groups {
127 0     0     my $self = shift;
128              
129 0           return map { $self->$_ } map { $_.'_groups' } qw/ major minor revision /
  0            
  0            
130             }
131              
132             has previous_version => (
133             is => 'ro',
134             lazy => 1,
135             default => sub {
136             my $self = shift;
137              
138             my $plugins =
139             $self->zilla->plugins_with('-YANICK::PreviousVersionProvider');
140              
141             $self->log_fatal(
142             "at least one plugin with the role PreviousVersionProvider",
143             "must be referenced in dist.ini"
144             ) unless ref $plugins and @$plugins >= 1;
145              
146             for my $plugin ( @$plugins ) {
147             my $version = $plugin->provide_previous_version;
148              
149             return $version if defined $version;
150             }
151              
152             return undef;
153             },
154             );
155              
156             sub provide_version {
157 0     0     my $self = shift;
158              
159             # override (or maybe needed to initialize)
160 0 0         return $ENV{V} if exists $ENV{V};
161              
162 0           my $new_ver = $self->next_version( $self->previous_version);
163              
164 0           $self->zilla->version("$new_ver");
165             }
166              
167             sub next_version {
168 0     0     my( $self, $last_version ) = @_;
169              
170              
171 0           my $new_ver = $self->increment_version( $self->increment_level );
172              
173 0 0         $new_ver = Perl::Version->new( $new_ver )->numify if $self->numify_version;
174              
175 1     1   2128 no warnings;
  1         2  
  1         186  
176 0           $self->log("Bumping version from $last_version to $new_ver");
177 0           return $new_ver;
178             }
179              
180             sub increment_level {
181 0     0     my ( $self ) = @_;
182              
183 0 0         my ($changes_file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files }
  0            
  0            
184             or die "no changelog file found\n";
185              
186 0           my $changes = CPAN::Changes->load_string( $changes_file->content,
187             next_token => qr/\{\{\$NEXT\}\}/ );
188              
189 0           my ($changelog) = reverse $changes->releases;
190              
191 0           my %category_map = (
192 0           map( { $_ => 0 } $self->major_groups ),
193 0           map( { $_ => 1 } $self->minor_groups ),
194             );
195              
196 0           $category_map{''} = $category_map{UNGROUPED};
197              
198 1     1   15 no warnings;
  1         2  
  1         220  
199              
200 0           my $increment_level = min
201 0           grep { defined }
202 0           map { $category_map{$_} }
203 0           grep { scalar @{ $changelog->changes($_) } } # only groups with items
  0            
204             $changelog->groups;
205              
206 0   0       return (qw/MAJOR MINOR PATCH/)[$increment_level//2];
207             }
208              
209             sub munge_files {
210 0     0     my ($self) = @_;
211              
212 0           my ($file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files };
  0            
  0            
213 0 0         return unless $file;
214              
215 0           my $changes = CPAN::Changes->load_string( $file->content,
216             next_token => qr/\{\{\$NEXT\}\}/
217             );
218              
219 0           my ( $next ) = reverse $changes->releases;
220              
221 0           $next->delete_group($_) for grep { !@{$next->changes($_)} } $next->groups;
  0            
  0            
222              
223 0           $self->log_debug([ 'updating contents of %s in memory', $file->name ]);
224 0           $file->content($changes->serialize);
225             }
226              
227              
228             __PACKAGE__->meta->make_immutable;
229 1     1   5 no Moose;
  1         1  
  1         5  
230              
231             {
232             package Dist::Zilla::Plugin::NextVersion::Semantic::Incrementer;
233             our $AUTHORITY = 'cpan:YANICK';
234             $Dist::Zilla::Plugin::NextVersion::Semantic::Incrementer::VERSION = '0.2.5';
235 1     1   202 use List::AllUtils qw/ first_index any /;
  1         1  
  1         72  
236              
237 1     1   663 use Moose::Role;
  1         3635  
  1         4  
238              
239             requires 'previous_version', 'format';
240              
241             sub nbr_version_levels {
242 0     0 0   my @tokens = $_[0]->format =~ /(%\d*d)/g;
243 0           return scalar @tokens;
244             }
245              
246             sub version_lenghts {
247 0     0 0   return $_[0]->format =~ /%0*(\d*)/g;
248             }
249              
250             sub increment_version {
251 0     0 0   my( $self, $level ) = @_;
252 0   0       $level ||= 'PATCH';
253              
254 0           my @version = (0,0,0);
255              
256             # initial version is special
257 0 0         if ( my $previous = $self->previous_version ) {
258 0           my $regex = quotemeta $self->format;
259 0           $regex =~ s/\\%0(\d+)d/(\\d{$1})/g;
260 0           $regex =~ s/\\%(\d+)d/(\\d{1,$1})/g;
261 0           $regex =~ s/\\%d/(\\d+)/g;
262              
263 0 0         @version = $previous =~ m/$regex/
264 0           or die "previous version '$previous' doesn't match format '@{[$self->format]}'" ;
265             }
266              
267 0           my @levels = qw/ MAJOR MINOR PATCH /;
268 0     0     my $index = first_index { $level eq $_ } @levels;
  0            
269              
270 0           $version[$index]++;
271 0           $version[$_] = 0 for $index+1..2;
272              
273             # if the incremental level is below the number of levels we
274             # have, increment the lowest level we consider
275 0 0   0     if( any { $version[$_] > 0 } $self->nbr_version_levels..2 ) {
  0            
276 0           $version[ $self->nbr_version_levels -1 ]++;
277 0           $version[$_] = 0 for $self->nbr_version_levels..2;
278             }
279              
280             # exceeding sizes?
281 0           my @sizes = $self->version_lenghts;
282 0           for my $i ( grep { $sizes[$_] } reverse 0..2 ) {
  0            
283 0 0         if ( length( $version[$i] ) > $sizes[$i] ) {
284 0           $version[$i-1]++;
285 0           $version[$i] = 0;
286             }
287             }
288              
289 1     1   4754 no warnings; # possible redundant args for sprintf
  1         2  
  1         146  
290              
291 0           my $version = sprintf $self->format, @version;
292 0           $version =~ y/ //d;
293              
294 0           return $version;
295             }
296              
297              
298             }
299              
300              
301             1;
302              
303             __END__
304              
305             =pod
306              
307             =encoding UTF-8
308              
309             =head1 NAME
310              
311             Dist::Zilla::Plugin::NextVersion::Semantic - update the next version, semantic-wise
312              
313             =head1 VERSION
314              
315             version 0.2.5
316              
317             =head1 SYNOPSIS
318              
319             # in dist.ini
320              
321             [NextVersion::Semantic]
322             major = MAJOR, API CHANGE
323             minor = MINOR, ENHANCEMENTS
324             revision = REVISION, BUG FIXES
325              
326             ; must also load a PreviousVersionProvider
327             [PreviousVersion::Changelog]
328              
329             =head1 DESCRIPTION
330              
331             Increases the distribution's version according to the semantic versioning rules
332             (see L<http://semver.org/>) by inspecting the changelog.
333              
334             More specifically, the plugin performs the following actions:
335              
336             =over
337              
338             =item at build time
339              
340             Reads the changelog using C<CPAN::Changes> and filters out of the C<{{$NEXT}}>
341             release section any group without item.
342              
343             =item before a release
344              
345             Ensures that there is at least one recorded change in the changelog, and
346             increments the version number in consequence. If there are changes given
347             outside of the sections, they are considered to be minor.
348              
349             =item after a release
350              
351             Updates the new C<{{$NEXT}}> section of the changelog with placeholders for
352             all the change categories. With categories as given in the I<SYNOPSIS>,
353             this would look like
354              
355             {{$NEXT}}
356              
357             [MAJOR]
358              
359             [API CHANGE]
360              
361             [MINOR]
362              
363             [ENHANCEMENTS]
364              
365             [REVISION]
366              
367             [BUG FIXES]
368              
369             =back
370              
371             If a version is given via the environment variable C<V>, it will taken
372             as-if as the next version.
373              
374             For this plugin to work, your L<Dist::Zilla> configuration must also contain a plugin
375             consuming the L<Dist::Zilla::Role::YANICK::PreviousVersionProvider> role.
376              
377             In the different configuration attributes where change group names are given,
378             the special group name C<UNGROUPED> can be given to
379             specify the nameless group.
380              
381             0.1.3 2013-07-18
382              
383             - this item will be part of UNGROUPED.
384              
385             [BUG FIXES]
386             - this one won't.
387              
388             =head1 PARAMETERS
389              
390             =head2 change_file
391              
392             File name of the changelog. Defaults to C<Changes>.
393              
394             =head2 numify_version
395              
396             If B<true>, the version will be a number using the I<x.yyyzzz> convention instead
397             of I<x.y.z>. Defaults to B<false>.
398              
399             =head2 format
400              
401             Specifies the version format to use. Follows the '%d' convention of
402             C<sprintf> (see examples below), excepts for one detail: '%3d' won't pad
403             with whitespaces, but will only determine the maximal size of the number.
404             If a version component exceeds its given
405             size, the next version level will be incremented.
406              
407             Examples:
408              
409             %d.%3d.%3d
410             PATCH LEVEL INCREASES: 0.0.998 -> 0.0.999 -> 0.1.0
411             MINOR LEVEL INCREASES: 0.0.8 -> 0.1.0 -> 0.2.0
412             MAJOR LEVEL INCREASES: 0.1.8 -> 1.0.0 -> 2.0.0
413              
414             %d.%02d%02d
415             PATCH LEVEL INCREASES: 0.0098 -> 0.00099 -> 0.0100
416             MINOR LEVEL INCREASES: 0.0008 -> 0.0100 -> 0.0200
417             MAJOR LEVEL INCREASES: 0.0108 -> 1.0000 -> 2.0000
418              
419             %d.%05d
420             MINOR LEVEL INCREASES: 0.99998 -> 0.99999 -> 1.00000
421             MAJOR LEVEL INCREASES: 0.00108 -> 1.00000 -> 2.00000
422              
423             Defaults to '%d.%3d.%3d'.
424              
425             =head2 major
426              
427             Comma-delimited list of categories of changes considered major.
428             Defaults to C<API CHANGES>.
429              
430             =head2 minor
431              
432             Comma-delimited list of categories of changes considered minor.
433             Defaults to C<ENHANCEMENTS> and C<UNGROUPED>.
434              
435             =head2 revision
436              
437             Comma-delimited list of categories of changes considered revisions.
438             Defaults to C<BUG FIXES, DOCUMENTATION>.
439              
440             =head1 AUTHOR
441              
442             Yanick Champoux <yanick@cpan.org>
443              
444             =head1 COPYRIGHT AND LICENSE
445              
446             This software is copyright (c) 2012 by Yanick Champoux.
447              
448             This is free software; you can redistribute it and/or modify it under
449             the same terms as the Perl 5 programming language system itself.
450              
451             =cut