File Coverage

blib/lib/Perl/Critic/Git.pm
Criterion Covered Total %
statement 89 89 100.0
branch 18 24 75.0
condition 10 19 52.6
subroutine 18 18 100.0
pod 7 7 100.0
total 142 157 90.4


line stmt bran cond sub pod time code
1             package Perl::Critic::Git;
2              
3 10     10   43499 use warnings;
  10         14  
  10         291  
4 10     10   38 use strict;
  10         10  
  10         193  
5              
6 10     10   33 use Carp;
  10         14  
  10         545  
7 10     10   4426 use Data::Dumper;
  10         74289  
  10         618  
8 10     10   67 use File::Basename qw();
  10         13  
  10         182  
9 10     10   5493 use Git::Repository qw( Blame );
  10         413433  
  10         47  
10 10     10   166711 use Perl::Critic qw();
  10         8022218  
  10         8895  
11              
12              
13             =head1 NAME
14              
15             Perl::Critic::Git - Bond git and Perl::Critic to blame the right people for violations.
16              
17              
18             =head1 VERSION
19              
20             Version 1.3.0
21              
22             =cut
23              
24             our $VERSION = '1.3.0';
25              
26              
27             =head1 SYNOPSIS
28              
29             use Perl::Critic::Git;
30             my $git_critic = Perl::Critic::Git->new(
31             file => $file,
32             level => $critique_level, # or undef to use default profile
33             );
34              
35             my $violations = $git_critic->report_violations(
36             author => $author, # or undef for all
37             since => $date, # to critique only recent changes
38             );
39              
40              
41             =head1 METHODS
42              
43             =head2 new()
44              
45             Create a new Perl::Critic::Git object.
46              
47             my $git_critic = Perl::Critic::Git->new(
48             file => $file,
49             level => $critique_level, # or undef to use default profile
50             );
51              
52             Parameters:
53              
54             =over 4
55              
56             =item * 'file'
57              
58             Mandatory, the path to a file in a Git repository.
59              
60             =item * 'level'
61              
62             Optional, to set a PerlCritic level. If it is not specified, the default
63             PerlCritic profile for the system will be used.
64              
65             #TODO: List allowed values from PerlCritic.
66              
67             =back
68              
69             =cut
70              
71             sub new
72             {
73 13     13 1 72104 my ( $class, %args ) = @_;
74 13         71 my $file = delete( $args{'file'} );
75 13         57 my $level = delete( $args{'level'} );
76              
77             # Check parameters.
78 13 100 66     210 croak "Argument 'file' is needed to create a Perl::Critic::Git object"
79             if !defined( $file ) || ( $file eq '' );
80 12 100       396 croak "Argument 'file' is not a valid file path"
81             unless -e $file;
82 11 100 100     214 croak "Argument 'level' is not a valid PerlCritic level"
83             if defined( $level ) && ( $level !~ /(?:gentle|5|stern|4|harsh|3|cruel|2|brutal|1)/x );
84              
85             # Create the object.
86 10         183 my $self = bless(
87             {
88             'file' => $file,
89             'level' => $level,
90             'analysis_completed' => 0,
91             'git_output' => undef,
92             'perlcritic_output' => undef,
93             'authors' => undef,
94             },
95             $class
96             );
97              
98 10         83 return $self;
99             }
100              
101              
102             =head2 get_authors()
103              
104             Return an arrayref of all the authors found in git blame for the file analyzed.
105              
106             my $authors = $git_critic->get_authors();
107              
108             =cut
109              
110             sub get_authors
111             {
112 3     3 1 394 my ( $self ) = @_;
113              
114 3 50       24 unless ( defined( $self->{'authors'} ) )
115             {
116 3         20 my $blame_lines = $self->get_blame_lines();
117              
118             # Find all the authors listed.
119 3         66 my $authors = {};
120 3         49 foreach my $blame_line ( @$blame_lines )
121             {
122 33         59 my $commit_attributes = $blame_line->get_commit_attributes();
123 33         107 $authors->{ $commit_attributes->{'author-mail'} } = 1;
124             }
125 3         15 $self->{'authors'} = [ keys %$authors ];
126             }
127              
128 3         50 return $self->{'authors'};
129             }
130              
131              
132             =head2 report_violations()
133              
134             Report the violations for a given Git author.
135              
136             my $violations = $git_critic->report_violations(
137             author => $author, # or undef for all
138             since => $date, # to critique only recent changes
139             );
140              
141             Parameters:
142              
143             =over 4
144              
145             =item * author (mandatory)
146              
147             The name of the author to search violations for.
148              
149             =item * since (optional)
150              
151             A date (format YYYY-MM-DD) for which violations of the PBPs that are older will
152             be ignored. This allows critiquing only recent changes, instead of forcing your
153             author to fix an entire legacy file at once if only one line needs to be
154             modified.
155              
156             =item * use_cache (default: 0)
157              
158             Use a cached version of C<git diff> when available. See
159             L<Git::Repository::Plugin::Blame::Cache> for more information.
160              
161             =back
162              
163             =cut
164              
165             sub report_violations
166             {
167 2     2 1 2653 my ( $self, %args ) = @_;
168 2         8 my $author = delete( $args{'author'} );
169 2         6 my $since = delete( $args{'since'} );
170 2   50     17 my $use_cache = delete( $args{'use_cache'} ) || 0;
171              
172             # Verify parameters.
173 2 50       12 croak 'The argument "author" must be passed'
174             if !defined( $author );
175              
176             # Analyze the file.
177 2         17 $self->_analyze_file(
178             use_cache => $use_cache,
179             );
180              
181             # Run through all the violations and find the ones from the author we're
182             # interested in.
183 2         18 my $author_violations = [];
184 2         69 my $perlcritic_violations = $self->get_perlcritic_violations();
185 2         5 foreach my $violation ( @$perlcritic_violations )
186             {
187 4         21 my $line_number = $violation->line_number();
188 4         37 my $blame_line = $self->get_blame_line( $line_number );
189 4         15 my $commit_attributes = $blame_line->get_commit_attributes();
190              
191             # If the author doesn't match, skip.
192 4 100       37 next unless $commit_attributes->{'author-mail'} eq $author;
193              
194             # If the parameters require filtering by time, do this here before we
195             # add it to the list of violations.
196 2 50 33     7 next if defined( $since ) && $commit_attributes->{'author-time'} < $since;
197              
198             # It passes all the search criteria, add it to the list.
199 2         7 push( @$author_violations, $violation );
200             }
201              
202 2         12 return $author_violations;
203             }
204              
205              
206             =head2 force_reanalyzing()
207              
208             Force reanalyzing the file specified by the current object. This is useful
209             if the file has been modified since the Perl::Critic::Git object has been
210             created.
211              
212             $git_critic->force_reanalyzing();
213              
214             =cut
215              
216             sub force_reanalyzing
217             {
218 1     1 1 358 my ( $self ) = @_;
219              
220 1         8 $self->_is_analyzed( 0 );
221              
222 1         4 return 1;
223             }
224              
225              
226             =head1 ACCESSORS
227              
228             =head2 get_perlcritic_violations()
229              
230             Return an arrayref of all the Perl::Critic::Violation objects found by running
231             Perl::Critic on the file specified by the current object.
232              
233             my $perlcritic_violations = $git_critic->get_perlcritic_violations();
234              
235             =cut
236              
237             sub get_perlcritic_violations
238             {
239 3     3 1 446 my ( $self ) = @_;
240              
241             # Analyze the file.
242 3         13 $self->_analyze_file();
243              
244 3         27 return $self->{'perlcritic_violations'}
245             }
246              
247              
248             =head2 get_blame_lines()
249              
250             Return an arrayref of Git::Repository::Plugin::Blame::Line objects corresponding
251             to the lines in the file analyzed.
252              
253             my $blame_lines = $self->get_blame_lines();
254              
255             =cut
256              
257             sub get_blame_lines
258             {
259 20     20 1 853 my ( $self ) = @_;
260              
261             # Analyze the file.
262 20         54 $self->_analyze_file();
263              
264 20         118 return $self->{'git_blame_lines'};
265             }
266              
267              
268             =head2 get_blame_line()
269              
270             Return a Git::Repository::Plugin::Blame::Line object corresponding to the line
271             number passed as parameter.
272              
273             my $blame_line = $git_critic->get_blame_line( 5 );
274              
275             =cut
276              
277             sub get_blame_line
278             {
279 15     15 1 8067 my ( $self, $line_number ) = @_;
280              
281             # Verify parameters.
282 15 50 33     155 croak 'The first parameter must be an integer representing a line number in the file analyzed'
      33        
283             if !defined( $line_number ) || $line_number !~ m/^\d+$/x || $line_number == 0;
284              
285 15         32 my $blame_lines = $self->get_blame_lines();
286 15 50       27 croak 'The line number requested does not exist'
287             if $line_number > scalar( @$blame_lines );
288              
289 15         70 return $blame_lines->[ $line_number - 1 ];
290             }
291              
292              
293             =head1 INTERNAL METHODS
294              
295             =head2 _analyze_file()
296              
297             Run "git blame" and "PerlCritic" on the file specified by the current object
298             and caches the results to speed reports later.
299              
300             $git_critic->_analyze_file();
301              
302             Arguments:
303              
304             =over 4
305              
306             =item * use_cache (default: 0)
307              
308             Use a cached version of C<git diff> when available.
309              
310             =back
311              
312             =cut
313              
314             sub _analyze_file
315             {
316 25     25   54 my ( $self, %args ) = @_;
317 25   50     148 my $use_cache = delete( $args{'use_cache'} ) || 0;
318              
319             # If the file has already been analyzed, no need to do it again.
320             return
321 25 100       63 if $self->_is_analyzed();
322              
323 7         40 my $file = $self->_get_file();
324              
325             # Git::Repository uses GIT_DIR and GIT_WORK_TREE to determine the path
326             # to the git repository when those environment variables are present.
327             # This however poses problems here, when those variables point to a
328             # different repository then the one the file to analyze belongs to,
329             # or when they use relative paths.
330             # To force Git::Repository to derive the git repository's path from
331             # the file path, we thus locally delete GIT_DIR and GIT_WORK_TREE.
332 7         420 local %ENV = %ENV;
333 7         59 delete( $ENV{'GIT_DIR'} );
334 7         38 delete( $ENV{'GIT_WORK_TREE'} );
335              
336             # Do a git blame on the file.
337 7         357 my ( undef, $directory, undef ) = File::Basename::fileparse( $file );
338 7         133 my $repository = Git::Repository->new( work_tree => $directory );
339 7         545778 $self->{'git_blame_lines'} = $repository->blame(
340             $file,
341             use_cache => $use_cache,
342             );
343              
344             # Run PerlCritic on the file.
345 7 50       175990 my $critic = Perl::Critic->new(
346             '-severity' => defined( $self->_get_critique_level() )
347             ? $self->_get_critique_level()
348             : undef,
349             );
350 7         2600212 $self->{'perlcritic_violations'} = [ $critic->critique( $file ) ];
351              
352             # Flag the file as analyzed.
353 7         187470 $self->_is_analyzed( 1 );
354              
355 7         7694 return;
356             }
357              
358              
359             =head2 _is_analyzed()
360              
361             Return whether the file specified by the current object has already been
362             analyzed with "git blame" and "PerlCritic".
363              
364             my $is_analyzed = $git_critic->_is_analyzed();
365              
366             =cut
367              
368             sub _is_analyzed
369             {
370 37     37   397 my ( $self, $value ) = @_;
371              
372 37 100       117 $self->{'analysis_completed'} = $value
373             if defined( $value );
374              
375 37         161 return $self->{'analysis_completed'};
376             }
377              
378              
379             =head2 _get_file()
380              
381             Return the path to the file to analyze for the current object.
382              
383             my $file = $git_critic->_get_file();
384              
385             =cut
386              
387             sub _get_file
388             {
389 8     8   326 my ( $self ) = @_;
390              
391 8         42 return $self->{'file'};
392             }
393              
394              
395             =head2 _get_critique_level()
396              
397             Return the critique level selected when creating the current object.
398              
399             my $critique_level = $git_critic->_get_critique_level();
400              
401             =cut
402              
403             sub _get_critique_level
404             {
405 14     14   31 my ( $self ) = @_;
406              
407 14         187 return $self->{'level'};
408             }
409              
410              
411             =head1 SEE ALSO
412              
413             =over 4
414              
415             =item * L<Perl::Critic>
416              
417             =back
418              
419              
420             =head1 BUGS
421              
422             Please report any bugs or feature requests through the web interface at
423             L<https://github.com/guillaumeaubert/Perl-Critic-Git/issues>.
424             I will be notified, and then you'll automatically be notified of progress on
425             your bug as I make changes.
426              
427              
428             =head1 SUPPORT
429              
430             You can find documentation for this module with the perldoc command.
431              
432             perldoc Perl::Critic::Git
433              
434              
435             You can also look for information at:
436              
437             =over 4
438              
439             =item * GitHub (report bugs there)
440              
441             L<https://github.com/guillaumeaubert/Perl-Critic-Git/issues>
442              
443             =item * AnnoCPAN: Annotated CPAN documentation
444              
445             L<http://annocpan.org/dist/Perl-Critic-Git>
446              
447             =item * CPAN Ratings
448              
449             L<http://cpanratings.perl.org/d/Perl-Critic-Git>
450              
451             =item * MetaCPAN
452              
453             L<https://metacpan.org/release/Perl-Critic-Git>
454              
455             =back
456              
457              
458             =head1 AUTHOR
459              
460             L<Guillaume Aubert|https://metacpan.org/author/AUBERTG>,
461             C<< <aubertg at cpan.org> >>.
462              
463              
464             =head1 ACKNOWLEDGEMENTS
465              
466             I originally developed this project for ThinkGeek
467             (L<http://www.thinkgeek.com/>). Thanks for allowing me to open-source it!
468              
469              
470             =head1 COPYRIGHT & LICENSE
471              
472             Copyright 2012-2017 Guillaume Aubert.
473              
474             This code is free software; you can redistribute it and/or modify it under the
475             same terms as Perl 5 itself.
476              
477             This program is distributed in the hope that it will be useful, but WITHOUT ANY
478             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
479             PARTICULAR PURPOSE. See the LICENSE file for more details.
480              
481             =cut
482              
483             1;