File Coverage

blib/lib/Dist/Zilla/Plugin/ChangeStats/Dependencies/Git.pm
Criterion Covered Total %
statement 38 143 26.5
branch 0 60 0.0
condition 0 20 0.0
subroutine 13 21 61.9
pod 0 5 0.0
total 51 249 20.4


line stmt bran cond sub pod time code
1 1     1   76525 use 5.10.1;
  1         2  
2 1     1   3 use strict;
  1         1  
  1         15  
3 1     1   2 use warnings;
  1         1  
  1         49  
4              
5             package Dist::Zilla::Plugin::ChangeStats::Dependencies::Git;
6              
7             # ABSTRACT: Add dependency changes to the changelog
8             our $AUTHORITY = 'cpan:CSSON'; # AUTHORITY
9             our $VERSION = '0.0200';
10              
11 1     1   467 use Moose;
  1         283763  
  1         4  
12 1     1   5044 use namespace::autoclean;
  1         1070  
  1         4  
13 1     1   508 use Types::Standard qw/ArrayRef Bool HashRef Str/;
  1         46221  
  1         10  
14 1     1   1166 use Git::Repository;
  1         20446  
  1         3  
15 1     1   403 use Module::CPANfile;
  1         10594  
  1         25  
16 1     1   695 use Path::Tiny;
  1         7563  
  1         48  
17 1     1   13 use Try::Tiny;
  1         2  
  1         45  
18 1     1   482 use CPAN::Changes;
  1         13866  
  1         29  
19 1     1   6 use CPAN::Changes::Group;
  1         1  
  1         17  
20 1     1   4 use JSON::MaybeXS qw/decode_json/;
  1         1  
  1         1147  
21              
22             with qw/
23             Dist::Zilla::Role::Plugin
24             Dist::Zilla::Role::FileMunger
25             /;
26              
27 0     0 0   sub mvp_multivalue_args { qw/stats_skip_file stats_skip_match/ }
28              
29             has repo => (
30             is => 'ro',
31             default => sub { Git::Repository->new(work_tree => '.')},
32             );
33             has change_file => (
34             is => 'ro',
35             isa => Str,
36             default => 'Changes',
37             );
38             has group => (
39             is => 'ro',
40             isa => Str,
41             default => '',
42             );
43             has format_tag => (
44             is => 'ro',
45             isa => Str,
46             default => '%s',
47             );
48             has add_to_first_release => (
49             is => 'ro',
50             isa => Bool,
51             default => 0,
52             );
53              
54             has do_stats => (
55             is => 'ro',
56             isa => Bool,
57             default => 0,
58             );
59             has stats_skip_file => (
60             is => 'ro',
61             isa => ArrayRef[Str],
62             traits => ['Array'],
63             default => sub { [] },
64             handles => {
65             all_stats_skip_files => 'elements',
66             has_stats_skip_files => 'count',
67             }
68             );
69             has stats_skip_match => (
70             is => 'ro',
71             isa => ArrayRef[Str],
72             traits => ['Array'],
73             default => sub { [] },
74             handles => {
75             all_stats_skip_matches => 'elements',
76             has_stats_skip_matches => 'count',
77             }
78             );
79             has stats_text => (
80             is => 'ro',
81             isa => Str,
82             default => 'Code churn',
83             );
84              
85              
86              
87             sub munge_files {
88 0     0 0   my $self = shift;
89              
90 0           my($file) = grep { $_->name eq $self->change_file } @{ $self->zilla->files };
  0            
  0            
91              
92 0 0         if(!defined $file) {
93 0           $self->log(['Could not find changelog (%s) - nothing to do', $self->change_file]);
94 0           return;
95             }
96              
97 0           my $changes = CPAN::Changes->load_string($file->content, next_token => $self->_next_token);
98 0           my($this_release) = ($changes->releases)[-1];
99 0 0         if($this_release->version ne '{{$NEXT}}') {
100 0           $self->log(['Could not find {{$NEXT}} token - skips']);
101 0           return;
102             }
103              
104 0 0         if(!path('META.json')->exists) {
105 0           $self->log(['Could not find META.json in distribution root - skips']);
106 0           return;
107             }
108 0           my $current_meta = decode_json(path('META.json')->slurp)->{'prereqs'};
109              
110 0           my($previous_release) = grep { $_->version ne '{{$NEXT}}' } reverse $changes->releases;
  0            
111              
112 0 0         my $is_first_release = defined $previous_release ? 0 : 1;
113              
114 0           my $tag_meta;
115             my $git_tag;
116 0 0 0       if($self->add_to_first_release && $is_first_release) {
    0          
117 0           $self->log(['First release - adds all dependencies']);
118 0           $tag_meta = {}; # fake meta
119             }
120             elsif($is_first_release) {
121 0           $self->log(['Has no earlier versions in changelog - no dependency changes']);
122 0           return;
123             }
124             else {
125 0           $self->log_debug(['Will compare dependencies with %s'], $previous_release->version);
126 0           $git_tag = sprintf $self->format_tag, $previous_release->version;
127              
128 0           $tag_meta = $self->get_meta($git_tag);
129 0 0 0       if(!defined $tag_meta || !defined $current_meta) {
130 0           return;
131             }
132             }
133              
134 0           my @all_requirement_changes = ();
135              
136             PHASE:
137 0           for my $phase (qw/runtime test build configure develop/) {
138             RELATION:
139 0           for my $relation (qw/requires recommends suggests/) {
140 0           my $requirement_changes = {
141             added => [],
142             changed => [],
143             removed => [],
144             };
145              
146 0   0       my $prev = $tag_meta->{ $phase }{ $relation } || {};
147 0   0       my $now = $current_meta->{ $phase }{ $relation } || {};
148              
149 0 0 0       next RELATION if !scalar keys %{ $prev } && !scalar keys %{ $now };
  0            
  0            
150              
151             # What is in the current release that wasn't in (or has changed since) the last release.
152             MODULE:
153 0           for my $module (sort keys %{ $now }) {
  0            
154 0   0       my $current_version = delete $now->{ $module } || '(any)';
155 0 0         my $previous_version = exists $prev->{ $module } ? delete $prev->{ $module } : undef;
156              
157 0 0         if(!defined $previous_version) {
158 0           push @{ $requirement_changes->{'added'} } => "$module $current_version";
  0            
159 0           next MODULE;
160             }
161              
162 0   0       $previous_version = $previous_version || '(any)';
163 0 0         if($current_version ne $previous_version) {
164 0           push @{ $requirement_changes->{'changed'} } => "$module $previous_version --> $current_version";
  0            
165             }
166             }
167             # What was in the last release that currenly isn't there
168 0           for my $module (sort keys %{ $prev }) {
  0            
169 0           push @{ $requirement_changes->{'removed'} } => $module;
  0            
170             }
171              
172             # Add requirement changes to overall list
173 0           for my $type (qw/added changed removed/) {
174 0 0         my $char = $type eq 'added' ? '+' : $type eq 'changed' ? '~' : $type eq 'removed' ? '-' : '!';
    0          
    0          
175              
176 0           for my $module (@{ $requirement_changes->{ $type }}) {
  0            
177 0           push @all_requirement_changes => ($self->phase_relation($phase, $relation) . " $char $module");
178             }
179             }
180             }
181             }
182              
183 0           my $group = $this_release->get_group($self->group);
184 0 0 0       $self->add_stats($group, $git_tag) if !$is_first_release && $self->do_stats;
185 0           $group->add_changes(@all_requirement_changes);
186 0           $file->content($changes->serialize);
187             }
188              
189             sub get_meta {
190 0     0 0   my $self = shift;
191 0           my $tag = shift;
192              
193 0           my(@tags) = $self->repo->run('tag');
194 0           my($found) = grep { $_ eq $tag } @tags;
  0            
195              
196 0 0         if(!$found) {
197 0           $self->log(['Could not find tag %s - skipping', $tag]);
198 0           return;
199             }
200              
201 0           my $show_output;
202             try {
203 0     0     ($show_output) = join '' => $self->repo->run('show', join ':' => ($tag, 'META.json'));
204             }
205             catch {
206 0 0   0     if($_ =~ m{^fatal:}) {
207 0           $self->log(['Could not find META.json in %s - skipping', $tag]);
208             }
209 0           die $_;
210 0           };
211 0 0         return if !defined $show_output;
212 0           return decode_json($show_output)->{'prereqs'};
213             }
214              
215             sub phase_relation {
216 0     0 0   my $self = shift;
217 0           my $phase = shift;
218 0           my $relation = shift;
219              
220 0 0         $phase = $phase eq 'runtime' ? 'run'
    0          
    0          
    0          
221             : $phase eq 'test' ? 'test'
222             : $phase eq 'configure' ? 'conf'
223             : $phase eq 'develop' ? 'dev'
224             : $phase
225             ;
226 0           $relation = substr $relation, 0, 3;
227              
228 0           return "($phase $relation)";
229             }
230              
231 0     0     sub _next_token { qr/\{\{\$NEXT\}\}/ }
232              
233             sub add_stats {
234 0     0 0   my $self = shift;
235 0           my $group = shift;
236 0           my $git_tag = shift;
237              
238 0           my @numstats = $self->repo->run(qw/diff --numstat/, $git_tag);
239 0           my $counter = {
240             files => 0,
241             insertions => 0,
242             deletions => 0,
243             };
244              
245             FILE:
246 0           for my $file (@numstats) {
247 0           my($insertions, $deletions, $path) = split /\s+/, $file, 3;
248 0 0         next FILE if grep { $path eq $_ } $self->all_stats_skip_files;
  0            
249 0 0         next FILE if grep { $path =~ m{$_}i } $self->all_stats_skip_matches;
  0            
250              
251             # binary files get '-'
252 0           ++$counter->{'files'};
253 0 0         $counter->{'insertions'} += $insertions =~ m{^\d+$} ? $insertions : 0;
254 0 0         $counter->{'deletions'} += $deletions =~ m{^\d+$} ? $deletions : 0;
255             }
256              
257             my $output = sprintf '%d file%s changed, %d insertion%s(+), %d deletion%s(-)',
258             $counter->{'files'},
259             $counter->{'files'} == 1 ? '': 's',
260             $counter->{'insertions'},
261             $counter->{'insertions'} == 1 ? '': 's',
262             $counter->{'deletions'},
263 0 0         $counter->{'deletions'} == 1 ? '': 's';
    0          
    0          
264              
265 0 0         my $intro = length $self->stats_text ? $self->stats_text . ': ' : '';
266              
267 0           $group->add_changes($intro . $output);
268             }
269              
270             __PACKAGE__->meta->make_immutable;
271              
272             1;
273              
274             __END__
275              
276             =pod
277              
278             =encoding UTF-8
279              
280             =head1 NAME
281              
282             Dist::Zilla::Plugin::ChangeStats::Dependencies::Git - Add dependency changes to the changelog
283              
284              
285              
286             =begin html
287              
288             <p>
289             <img src="https://img.shields.io/badge/perl-5.10+-blue.svg" alt="Requires Perl 5.10+" />
290             <a href="https://travis-ci.org/Csson/p5-Dist-Zilla-Plugin-ChangeStats-Dependencies-Git"><img src="https://api.travis-ci.org/Csson/p5-Dist-Zilla-Plugin-ChangeStats-Dependencies-Git.svg?branch=master" alt="Travis status" /></a>
291             <a href="http://cpants.cpanauthors.org/release/CSSON/Dist-Zilla-Plugin-ChangeStats-Dependencies-Git-0.0200"><img src="http://badgedepot.code301.com/badge/kwalitee/CSSON/Dist-Zilla-Plugin-ChangeStats-Dependencies-Git/0.0200" alt="Distribution kwalitee" /></a>
292             <a href="http://matrix.cpantesters.org/?dist=Dist-Zilla-Plugin-ChangeStats-Dependencies-Git%200.0200"><img src="http://badgedepot.code301.com/badge/cpantesters/Dist-Zilla-Plugin-ChangeStats-Dependencies-Git/0.0200" alt="CPAN Testers result" /></a>
293             <img src="https://img.shields.io/badge/coverage-20.5%-red.svg" alt="coverage 20.5%" />
294             </p>
295              
296             =end html
297              
298             =head1 VERSION
299              
300             Version 0.0200, released 2016-09-20.
301              
302             =head1 SYNOPSIS
303              
304             ; in dist.ini
305             [ChangeStats::Dependencies::Git]
306             group = Dependency Changes
307              
308             =head1 DESCRIPTION
309              
310             This plugin adds detailed information about changes in requirements to the changelog, possibly in a group. The
311             synopsis might add this:
312              
313             [Dependency Changes]
314             - (run req) + Moose (any)
315             - (run req) - No::Longer::Used
316             - (test sug) + Something::Useful 0.82
317             - (dev req) ~ List::Util 1.40 --> 1.42
318              
319             For this to work the following must be true:
320              
321             =over 4
322              
323             =item *
324              
325             The changelog must conform to L<CPAN::Changes::Spec>.
326              
327             =item *
328              
329             There must be a C<META.json> in both the working directory and in the tags.
330              
331             =item *
332              
333             Git tag names must be identical to (or a superset of) the version numbers in the changelog.
334              
335             =item *
336              
337             This plugin should come before [NextRelease] or similar in dist.ini.
338              
339             =back
340              
341             =head1 ATTRIBUTES
342              
343             =head2 change_file
344              
345             Default: C<Changes>
346              
347             The name of the changelog file.
348              
349             =head2 group
350              
351             Default: No group
352              
353             The group (if any) under which to add the dependency changes. If the group already exists these changes will be appended to that group.
354              
355             =head2 format_tag
356              
357             Default: C<%s>
358              
359             Use this if the Git tags are formatted differently to the versions in the changelog. C<%s> gets replaced with the version.
360              
361             =head1 SEE ALSO
362              
363             =over 4
364              
365             =item *
366              
367             L<Dist::Zilla::Plugin::ChangeStats::Git>
368              
369             =back
370              
371             =head1 SOURCE
372              
373             L<https://github.com/Csson/p5-Dist-Zilla-Plugin-ChangeStats-Dependencies-Git>
374              
375             =head1 HOMEPAGE
376              
377             L<https://metacpan.org/release/Dist-Zilla-Plugin-ChangeStats-Dependencies-Git>
378              
379             =head1 AUTHOR
380              
381             Erik Carlsson <info@code301.com>
382              
383             =head1 COPYRIGHT AND LICENSE
384              
385             This software is copyright (c) 2016 by Erik Carlsson.
386              
387             This is free software; you can redistribute it and/or modify it under
388             the same terms as the Perl 5 programming language system itself.
389              
390             =cut