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   82215 use warnings;
  10         23  
  10         438  
4 10     10   50 use strict;
  10         18  
  10         290  
5              
6 10     10   51 use Carp;
  10         22  
  10         947  
7 10     10   15268 use Data::Dumper;
  10         107627  
  10         654  
8 10     10   111 use File::Basename qw();
  10         18  
  10         224  
9 10     10   10640 use Git::Repository qw( Blame );
  10         366234  
  10         135  
10 10     10   670426 use Perl::Critic qw();
  10         36781127  
  10         11978  
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.2.1
21              
22             =cut
23              
24             our $VERSION = '1.2.1';
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 689737 my ( $class, %args ) = @_;
74 13         192 my $file = delete( $args{'file'} );
75 13         98 my $level = delete( $args{'level'} );
76              
77             # Check parameters.
78 13 100 66     344 croak "Argument 'file' is needed to create a Perl::Critic::Git object"
79             if !defined( $file ) || ( $file eq '' );
80 12 100       771 croak "Argument 'file' is not a valid file path"
81             unless -e $file;
82 11 100 100     303 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         355 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         94 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 375 my ( $self ) = @_;
113              
114 3 50       37 unless ( defined( $self->{'authors'} ) )
115             {
116 3         41 my $blame_lines = $self->get_blame_lines();
117              
118             # Find all the authors listed.
119 3         63 my $authors = {};
120 3         29 foreach my $blame_line ( @$blame_lines )
121             {
122 33         91 my $commit_attributes = $blame_line->get_commit_attributes();
123 33         185 $authors->{ $commit_attributes->{'author-mail'} } = 1;
124             }
125 3         20 $self->{'authors'} = [ keys %$authors ];
126             }
127              
128 3         103 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 1653 my ( $self, %args ) = @_;
168 2         8 my $author = delete( $args{'author'} );
169 2         7 my $since = delete( $args{'since'} );
170 2   50     18 my $use_cache = delete( $args{'use_cache'} ) || 0;
171              
172             # Verify parameters.
173 2 50       8 croak 'The argument "author" must be passed'
174             if !defined( $author );
175              
176             # Analyze the file.
177 2         11 $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         54 my $author_violations = [];
184 2         8 my $perlcritic_violations = $self->get_perlcritic_violations();
185 2         6 foreach my $violation ( @$perlcritic_violations )
186             {
187 4         47 my $line_number = $violation->line_number();
188 4         30 my $blame_line = $self->get_blame_line( $line_number );
189 4         17 my $commit_attributes = $blame_line->get_commit_attributes();
190              
191             # If the author doesn't match, skip.
192 4 100       32 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     8 next if defined( $since ) && $commit_attributes->{'author-time'} < $since;
197              
198             # It passes all the search criteria, add it to the list.
199 2         6 push( @$author_violations, $violation );
200             }
201              
202 2         13 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 518 my ( $self ) = @_;
219              
220 1         16 $self->_is_analyzed( 0 );
221              
222 1         12 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 371 my ( $self ) = @_;
240              
241             # Analyze the file.
242 3         21 $self->_analyze_file();
243              
244 3         42 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 872 my ( $self ) = @_;
260              
261             # Analyze the file.
262 20         79 $self->_analyze_file();
263              
264 20         186 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 12460 my ( $self, $line_number ) = @_;
280              
281             # Verify parameters.
282 15 50 33     183 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         45 my $blame_lines = $self->get_blame_lines();
286 15 50       79 croak 'The line number requested does not exist'
287             if $line_number > scalar( @$blame_lines );
288              
289 15         76 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   64 my ( $self, %args ) = @_;
317 25   50     180 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       102 if $self->_is_analyzed();
322              
323 7         68 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         471 local %ENV = %ENV;
333 7         99 delete( $ENV{'GIT_DIR'} );
334 7         53 delete( $ENV{'GIT_WORK_TREE'} );
335              
336             # Do a git blame on the file.
337 7         648 my ( undef, $directory, undef ) = File::Basename::fileparse( $file );
338 7         164 my $repository = Git::Repository->new( work_tree => $directory );
339 7         1029158 $self->{'git_blame_lines'} = $repository->blame(
340             $file,
341             use_cache => $use_cache,
342             );
343              
344             # Run PerlCritic on the file.
345 7 50       291806 my $critic = Perl::Critic->new(
346             '-severity' => defined( $self->_get_critique_level() )
347             ? $self->_get_critique_level()
348             : undef,
349             );
350 7         4537022 $self->{'perlcritic_violations'} = [ $critic->critique( $file ) ];
351              
352             # Flag the file as analyzed.
353 7         295388 $self->_is_analyzed( 1 );
354              
355 7         12463 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   408 my ( $self, $value ) = @_;
371              
372 37 100       436 $self->{'analysis_completed'} = $value
373             if defined( $value );
374              
375 37         1255 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   1538 my ( $self ) = @_;
390              
391 8         58 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   97 my ( $self ) = @_;
406              
407 14         405 return $self->{'level'};
408             }
409              
410              
411             =head1 BUGS
412              
413             Please report any bugs or feature requests through the web interface at
414             L<https://github.com/guillaumeaubert/Perl-Critic-Git/issues>.
415             I will be notified, and then you'll automatically be notified of progress on
416             your bug as I make changes.
417              
418              
419             =head1 SUPPORT
420              
421             You can find documentation for this module with the perldoc command.
422              
423             perldoc Perl::Critic::Git
424              
425              
426             You can also look for information at:
427              
428             =over 4
429              
430             =item * GitHub (report bugs there)
431              
432             L<https://github.com/guillaumeaubert/Perl-Critic-Git/issues>
433              
434             =item * AnnoCPAN: Annotated CPAN documentation
435              
436             L<http://annocpan.org/dist/Perl-Critic-Git>
437              
438             =item * CPAN Ratings
439              
440             L<http://cpanratings.perl.org/d/Perl-Critic-Git>
441              
442             =item * MetaCPAN
443              
444             L<https://metacpan.org/release/Perl-Critic-Git>
445              
446             =back
447              
448              
449             =head1 AUTHOR
450              
451             L<Guillaume Aubert|https://metacpan.org/author/AUBERTG>,
452             C<< <aubertg at cpan.org> >>.
453              
454              
455             =head1 ACKNOWLEDGEMENTS
456              
457             I originally developed this project for ThinkGeek
458             (L<http://www.thinkgeek.com/>). Thanks for allowing me to open-source it!
459              
460              
461             =head1 COPYRIGHT & LICENSE
462              
463             Copyright 2012-2014 Guillaume Aubert.
464              
465             This program is free software: you can redistribute it and/or modify it under
466             the terms of the GNU General Public License version 3 as published by the Free
467             Software Foundation.
468              
469             This program is distributed in the hope that it will be useful, but WITHOUT ANY
470             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
471             PARTICULAR PURPOSE. See the GNU General Public License for more details.
472              
473             You should have received a copy of the GNU General Public License along with
474             this program. If not, see http://www.gnu.org/licenses/
475              
476             =cut
477              
478             1;