File Coverage

blib/lib/Dist/Zilla/Plugin/NextVersion/Semantic.pm
Criterion Covered Total %
statement 117 133 87.9
branch 14 22 63.6
condition 4 4 100.0
subroutine 25 27 92.5
pod 0 10 0.0
total 160 196 81.6


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