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 24 26 92.3
pod 0 10 0.0
total 159 195 81.5


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.6';
5 2     2   85403 use strict;
  2         12  
  2         64  
6 2     2   11 use warnings;
  2         3  
  2         51  
7              
8 2     2   26 use 5.10.0;
  2         6  
9              
10 2     2   436 use CPAN::Changes 0.20;
  2         21839  
  2         63  
11 2     2   950 use Perl::Version;
  2         6561  
  2         57  
12 2     2   1078 use List::AllUtils qw/ any min /;
  2         23152  
  2         170  
13              
14 2     2   530 use Moose;
  2         379026  
  2         15  
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 2     2   12598 use Moose::Util::TypeConstraints;
  2         5  
  2         21  
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 1     1 0 161932 my $self = shift;
82              
83 1         16 my ($changes_file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files };
  3         218  
  1         89  
84              
85 1         29 my $changes = CPAN::Changes->load_string(
86             $changes_file->content,
87             next_token => qr/\{\{\$NEXT\}\}/
88             );
89              
90 1         1186 my( $next ) = reverse $changes->releases;
91              
92 1         154 my @changes = values %{ $next->changes };
  1         14  
93              
94 1 50       49 $self->log_fatal("change file has no content for next version")
95             unless @changes;
96              
97             }
98              
99             sub after_release {
100 0     0 0 0 my ($self) = @_;
101 0         0 my $filename = $self->change_file;
102              
103 0         0 my $changes = CPAN::Changes->load(
104             $self->change_file,
105             next_token => qr/\{\{\$NEXT\}\}/
106             );
107              
108             # remove empty groups
109 0         0 $changes->delete_empty_groups;
110              
111 0         0 my ( $next ) = reverse $changes->releases;
112              
113 0         0 $next->add_group( grep { $_ ne 'UNGROUPED' } $self->all_groups );
  0         0  
114              
115 0         0 $self->log_debug([ 'updating contents of %s on disk', $filename ]);
116              
117             # and finally rewrite the changelog on disk
118 0 0       0 open my $out_fh, '>', $filename
119             or Carp::croak("can't open $filename for writing: $!");
120              
121 0         0 print $out_fh $changes->serialize;
122              
123 0 0       0 close $out_fh or Carp::croak("error closing $filename: $!");
124             }
125              
126             sub all_groups {
127 0     0 0 0 my $self = shift;
128              
129 0         0 return map { $self->$_ } map { $_.'_groups' } qw/ major minor revision /
  0         0  
  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 8     8 0 177790 my $self = shift;
158              
159             # override (or maybe needed to initialize)
160 8 100       34 return $ENV{V} if exists $ENV{V};
161              
162 7         263 my $new_ver = $self->next_version( $self->previous_version);
163              
164 6         183 $self->zilla->version("$new_ver");
165             }
166              
167             sub next_version {
168 6     6 0 18 my( $self, $last_version ) = @_;
169              
170              
171 6         19 my $new_ver = $self->increment_version( $self->increment_level );
172              
173 6 100       190 $new_ver = Perl::Version->new( $new_ver )->numify if $self->numify_version;
174              
175 2     2   5365 no warnings;
  2         11  
  2         575  
176 6         211 $self->log("Bumping version from $last_version to $new_ver");
177 6         1606 return $new_ver;
178             }
179              
180             sub increment_level {
181 6     6 0 12 my ( $self ) = @_;
182              
183 6 50       12 my ($changes_file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files }
  18         198  
  6         141  
184             or die "no changelog file found\n";
185              
186 6         23 my $changes = CPAN::Changes->load_string( $changes_file->content,
187             next_token => qr/\{\{\$NEXT\}\}/ );
188              
189 6         2091 my ($changelog) = reverse $changes->releases;
190              
191             my %category_map = (
192 6         225 map( { $_ => 0 } $self->major_groups ),
193 6         404 map( { $_ => 1 } $self->minor_groups ),
  12         42  
194             );
195              
196 6         15 $category_map{''} = $category_map{UNGROUPED};
197              
198 2     2   15 no warnings;
  2         4  
  2         527  
199              
200             my $increment_level = min
201 6         22 grep { defined }
202 6         74 map { $category_map{$_} }
203 6         24 grep { scalar @{ $changelog->changes($_) } } # only groups with items
  6         90  
  6         21  
204             $changelog->groups;
205              
206 6   100     92 return (qw/MAJOR MINOR PATCH/)[$increment_level//2];
207             }
208              
209             sub munge_files {
210 7     7 0 315851 my ($self) = @_;
211              
212 7         16 my ($file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files };
  21         231  
  7         168  
213 7 50       27 return unless $file;
214              
215 7         26 my $changes = CPAN::Changes->load_string( $file->content,
216             next_token => qr/\{\{\$NEXT\}\}/
217             );
218              
219 7         2982 my ( $next ) = reverse $changes->releases;
220              
221 7         470 $next->delete_group($_) for grep { !@{$next->changes($_)} } $next->groups;
  7         103  
  7         21  
222              
223 7         110 $self->log_debug([ 'updating contents of %s in memory', $file->name ]);
224 7         695 $file->content($changes->serialize);
225             }
226              
227              
228             __PACKAGE__->meta->make_immutable;
229 2     2   14 no Moose;
  2         5  
  2         11  
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.6';
235 2     2   544 use List::AllUtils qw/ first_index any /;
  2         5  
  2         116  
236              
237 2     2   584 use Moose::Role;
  2         4048  
  2         15  
238              
239             requires 'previous_version', 'format';
240              
241             sub nbr_version_levels {
242 29     29 0 498 my @tokens = $_[0]->format =~ /(%\d*d)/g;
243 29         263 return scalar @tokens;
244             }
245              
246             sub version_lenghts {
247 25     25 0 387 return $_[0]->format =~ /%0*(\d*)/g;
248             }
249              
250             sub increment_version {
251 25     25 0 29606 my( $self, $level ) = @_;
252 25   100     72 $level ||= 'PATCH';
253              
254 25         50 my @version = (0,0,0);
255              
256             # initial version is special
257 25 100       534 if ( my $previous = $self->previous_version ) {
258 22         431 my $regex = quotemeta $self->format;
259 22         131 $regex =~ s/\\%0(\d+)d/(\\d{$1})/g;
260 22         134 $regex =~ s/\\%(\d+)d/(\\d{1,$1})/g;
261 22         78 $regex =~ s/\\%d/(\\d+)/g;
262              
263 22 50       195 @version = $previous =~ m/$regex/
264 0         0 or die "previous version '$previous' doesn't match format '@{[$self->format]}'" ;
265             }
266              
267 25         82 my @levels = qw/ MAJOR MINOR PATCH /;
268 25     53   138 my $index = first_index { $level eq $_ } @levels;
  53         99  
269              
270 25         80 $version[$index]++;
271 25         88 $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 25 100   6   92 if( any { $version[$_] > 0 } $self->nbr_version_levels..2 ) {
  6         15  
276 2         5 $version[ $self->nbr_version_levels -1 ]++;
277 2         6 $version[$_] = 0 for $self->nbr_version_levels..2;
278             }
279              
280             # exceeding sizes?
281 25         86 my @sizes = $self->version_lenghts;
282 25         182 for my $i ( grep { $sizes[$_] } reverse 0..2 ) {
  75         153  
283 38 100       107 if ( length( $version[$i] ) > $sizes[$i] ) {
284 1         4 $version[$i-1]++;
285 1         2 $version[$i] = 0;
286             }
287             }
288              
289 2     2   9816 no warnings; # possible redundant args for sprintf
  2         6  
  2         253  
290              
291 25         402 my $version = sprintf $self->format, @version;
292 25         178 $version =~ y/ //d;
293              
294 25         79 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.6
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) 2021, 2015, 2014, 2013, 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