File Coverage

blib/lib/App/GitHooks.pm
Criterion Covered Total %
statement 207 222 93.2
branch 72 96 75.0
condition 24 42 57.1
subroutine 41 42 97.6
pod 19 19 100.0
total 363 421 86.2


line stmt bran cond sub pod time code
1             package App::GitHooks;
2              
3 31     31   29942 use strict;
  31         68  
  31         843  
4 31     31   122 use warnings;
  31         35  
  31         1286  
5              
6             # Unbuffer output to display it as quickly as possible.
7             local $| = 1;
8              
9             # External dependencies.
10 31     31   123 use Carp qw( carp croak );
  31         34  
  31         1812  
11 31     31   14849 use Class::Load qw();
  31         613891  
  31         944  
12 31     31   15208 use Config::Tiny qw();
  31         29303  
  31         681  
13 31     31   19486 use Data::Validate::Type qw();
  31         226691  
  31         1027  
14 31     31   21808 use Data::Dumper qw( Dumper );
  31         214503  
  31         2596  
15 31     31   220 use File::Basename qw();
  31         48  
  31         521  
16 31     31   14474 use Git::Repository qw();
  31         979415  
  31         971  
17             use Module::Pluggable
18 31         208 require => 1,
19 31     31   15103 sub_name => '_search_plugins';
  31         258321  
20 31     31   25377 use Term::ANSIColor qw();
  31         159010  
  31         978  
21 31     31   16890 use Text::Wrap qw();
  31         74923  
  31         851  
22 31     31   204 use Try::Tiny qw( try catch finally );
  31         40  
  31         1920  
23 31     31   19796 use Storable qw();
  31         90434  
  31         1011  
24              
25             # Internal dependencies.
26 31     31   14183 use App::GitHooks::Config;
  31         63  
  31         966  
27 31     31   10652 use App::GitHooks::Constants qw( :HOOK_EXIT_CODES );
  31         71  
  31         9375  
28 31     31   12764 use App::GitHooks::Plugin;
  31         55  
  31         814  
29 31     31   11880 use App::GitHooks::StagedChanges;
  31         88  
  31         1171  
30 31     31   15935 use App::GitHooks::Terminal;
  31         90  
  31         77563  
31              
32              
33             =head1 NAME
34              
35             App::GitHooks - Extensible plugins system for git hooks.
36              
37              
38             =head1 VERSION
39              
40             Version 1.9.0
41              
42             =cut
43              
44             our $VERSION = '1.9.0';
45              
46              
47             =head1 DESCRIPTION
48              
49             C<App::GitHooks> is an extensible and easy to configure git hooks framework that supports many plugins.
50              
51             Here's an example of it in action, running the C<pre-commit> hook checks before
52             the commit message can be entered:
53              
54             =begin html
55              
56             <div><img src="https://raw.github.com/guillaumeaubert/App-GitHooks/master/img/app-githooks-example-success.png"></div>
57              
58             =end html
59              
60             Here is another example, with a Perl file that fails compilation this time:
61              
62             =begin html
63              
64             <div><img src="https://raw.github.com/guillaumeaubert/App-GitHooks/master/img/app-githooks-example-failure.png"></div>
65              
66             =end html
67              
68              
69             =head1 SYNOPSIS
70              
71             =over 4
72              
73             =item 1.
74              
75             Install this distribution (with cpanm or your preferred CPAN client):
76              
77             cpanm App::GitHooks
78              
79             =item 2.
80              
81             Install the plugins you are interested in (with cpanm or your prefered CPAN
82             client), as C<App::GitHooks> does not bundle them. See the list of plugins
83             below, but for example:
84              
85             cpanm App::GitHooks::Plugin::BlockNOCOMMIT
86             cpanm App::GitHooks::Plugin::DetectCommitNoVerify
87             ...
88              
89             =item 3.
90              
91             Go to the git repository for which you want to set up git hooks, and run:
92              
93             githooks install
94              
95             =item 4.
96              
97             Enjoy!
98              
99             =back
100              
101              
102             =head1 GIT REQUIREMENTS
103              
104             L<App::GitHooks> requires git v1.7.4.1 or above.
105              
106              
107             =head1 VALID GIT HOOK NAMES
108              
109             =over 4
110              
111             =item * applypatch-msg
112              
113             =item * pre-applypatch
114              
115             =item * post-applypatch
116              
117             =item * pre-commit
118              
119             =item * prepare-commit-msg
120              
121             =item * commit-msg
122              
123             =item * post-commit
124              
125             =item * pre-rebase
126              
127             =item * post-checkout
128              
129             =item * post-merge
130              
131             =item * pre-receive
132              
133             =item * update
134              
135             =item * post-receive
136              
137             =item * post-update
138              
139             =item * pre-auto-gc
140              
141             =item * post-rewrite
142              
143             =back
144              
145             =cut
146              
147             # List of valid git hooks.
148             # From https://www.kernel.org/pub/software/scm/git/docs/githooks.html
149             our $HOOK_NAMES =
150             [
151             qw(
152             applypatch-msg
153             commit-msg
154             post-applypatch
155             post-checkout
156             post-commit
157             post-merge
158             post-receive
159             post-rewrite
160             post-update
161             pre-applypatch
162             pre-auto-gc
163             pre-commit
164             pre-push
165             pre-rebase
166             pre-receive
167             prepare-commit-msg
168             update
169             )
170             ];
171              
172              
173             =head1 OFFICIALLY SUPPORTED PLUGINS
174              
175             =over 4
176              
177             =item * L<App::GitHooks::Plugin::BlockNOCOMMIT>
178              
179             Prevent committing code with #NOCOMMIT mentions.
180              
181             =item * L<App::GitHooks::Plugin::BlockProductionCommits>
182              
183             Prevent commits in a production environment.
184              
185             =item * L<App::GitHooks::Plugin::DetectCommitNoVerify>
186              
187             Find out when someone uses --no-verify and append the pre-commit checks to the
188             commit message.
189              
190             =item * L<App::GitHooks::Plugin::ForceRegularUpdate>
191              
192             Force running a specific tool at regular intervals.
193              
194             =item * L<App::GitHooks::Plugin::MatchBranchTicketID>
195              
196             Detect discrepancies between the ticket ID specified by the branch name and the
197             one in the commit message.
198              
199             =item * L<App::GitHooks::Plugin::PerlCompile>
200              
201             Verify that Perl files compile without errors.
202              
203             =item * L<App::GitHooks::Plugin::PerlCritic>
204              
205             Verify that all changes and addition to the Perl files pass PerlCritic checks.
206              
207             =item * L<App::GitHooks::Plugin::PerlInterpreter>
208              
209             Enforce a specific Perl interpreter on the first line of Perl files.
210              
211             =item * L<App::GitHooks::Plugin::PgBouncerAuthSyntax>
212              
213             Verify that the syntax of PgBouncer auth files is correct.
214              
215             =item * L<App::GitHooks::Plugin::PrependTicketID>
216              
217             Derive a ticket ID from the branch name and prepend it to the commit-message.
218              
219             =item * L<App::GitHooks::Plugin::RequireCommitMessage>
220              
221             Require a commit message.
222              
223             =item * L<App::GitHooks::Plugin::RequireTicketID>
224              
225             Verify that staged Ruby files compile.
226              
227             =item * L<App::GitHooks::Plugin::ValidatePODFormat>
228              
229             Validate POD format in Perl and POD files.
230              
231             =back
232              
233              
234             =head1 CONTRIBUTED PLUGINS
235              
236             =over 4
237              
238             =item * L<App::GitHooks::Plugin::RubyCompile>
239              
240             Verify that staged Ruby files compile.
241              
242             =item * L<App::GitHooks::Plugin::PreventTrailingWhitespace>
243              
244             Prevent trailing whitespace from being committed.
245              
246             =back
247              
248              
249             =head1 CONFIGURATION OPTIONS
250              
251             =head2 Configuration format
252              
253             L<App::GitHooks> uses L<Config::Tiny>, so the configuration should follow the
254             following format:
255              
256             general_key_1 = value
257             general_key_2 = value
258              
259             [section_1]
260             section_1_key 1 = value
261              
262             The file is divided between the global configuration options at the beginning
263             of the file (such as C<general_key_1> above) and plugin specific configuration
264             options which are located in distinct sections (such as C<section_1_key> in the
265             C<[section_1]> section).
266              
267              
268             =head2 Configuration file locations
269              
270             L<App::GitHooks> supports setting custom options by creating one of the
271             following files, which are searched in descending order of preference:
272              
273             =over 4
274              
275             =item *
276              
277             A file of any name anywhere on your system, if you set the environment variable
278             C<GITHOOKSRC_FORCE> to its path.
279              
280             Note that you should normally use C<GITHOOKSRC>. This option is provided mostly
281             for testing purposes, when configuration options for testing in a reliable
282             manner are of the utmost importance and take precedence over any
283             repository-specific settings.
284              
285             =item *
286              
287             A C<.githooksrc> file at the root of the git repository.
288              
289             The settings will then only apply to that repository.
290              
291             =item *
292              
293             A file of any name anywhere on your system, if you set the environment variable
294             C<GITHOOKSRC> to its path.
295              
296             Note that C<.githooksrc> files at the top of a repository or in a user's home
297             directory will take precedence over a file specified by the C<GITHOOKSRC>
298             environment variable.
299              
300             =item *
301              
302             A C<.githooksrc> file in the home directory of the current user.
303              
304             The settings will then apply to all the repositories that have hooks set up.
305             Note that if C<.githooksrc> file is defined at the root of a repository, that
306             configuration file will take precedence over the one defined in the home
307             directory of the current user (as it is presumably more specific). Auto-merge
308             of options across multiple C<.githooksrc> files in an inheritance fashion is
309             not currently supported.
310              
311             =back
312              
313              
314             =head2 General configuration options
315              
316             =over 4
317              
318             =item * project_prefixes
319              
320             A comma-separated list of project prefixes, in case you want to use this in
321             C<extract_ticket_id_from_commit> or C<extract_ticket_id_from_branch>.
322              
323             project_prefixes = OPS, DEV
324              
325             =item * extract_ticket_id_from_commit
326              
327             A regular expression with _one_ capturing group that will be applied to the
328             first line of a commit message to extract the ticket ID referenced, if there is
329             one.
330              
331             extract_ticket_id_from_commit = /^($project_prefixes-\d+|--): /
332              
333             =item * extract_ticket_id_from_branch
334              
335             A regular expression with _one_ capturing group that will be applied to branch
336             names to extract a ticket ID. This allows creating one branch per ticket and
337             having the hooks check that the commit messages and the branch names are in
338             sync.
339              
340             extract_ticket_id_from_branch = /^($project_prefixes-?\d+)/
341              
342             =item * normalize_branch_ticket_id
343              
344             A replacement expression that normalizes the ticket ID captured with
345             C<extract_ticket_id_from_branch>.
346              
347             normalize_branch_ticket_id = s/^(.*?)-?(\d+)$/\U$1-$2/
348              
349             =item * skip_directories
350              
351             A regular expression to filter the directory names that should be skipped when
352             analyzing files as part of file-level checks.
353              
354             skip_directories = /^cpan(?:-[^\/]+)?\//
355              
356             =item * force_plugins
357              
358             A comma-separated list of the plugins that must be present on the system and
359             will be executed. If any plugins from this list are missing, the action will
360             error out. If any other plugins not in this list are installed on the system,
361             they will be ignored.
362              
363             force_plugins = App::GitHooks::Plugin::ValidatePODFormat, App::GitHooks::Plugin::RequireCommitMessage
364              
365             =item * min_app_githooks_version
366              
367             Specify the minimum version of App::GitHooks.
368              
369             min_app_githooks_version = 1.9.0
370              
371             =back
372              
373              
374             =head2 Testing-specific options
375              
376             =over 4
377              
378             =item * limit_plugins
379              
380             Deprecated. Use C<force_plugins> instead.
381              
382             =item * force_interactive
383              
384             Force the application to consider that the terminal is interactive (`1`) or
385             non-interactive (`0`) independently of whether the actual STDOUT is interactive
386             or not.
387              
388             =item * force_use_colors
389              
390             Force the output to use colors (`1`) or to not use colors (`0`) independently
391             of the ability of STDOUT to display colors.
392              
393             =item * force_is_utf8
394              
395             Allows the output to use utf-8 characters (`1`) or not (`0`), independently of
396             whether the output declares supporting utf-8.
397              
398             =item * commit_msg_no_edit
399              
400             Allows skipping the loop to edit the message when the commit message checks
401             failed.
402              
403             =back
404              
405              
406             =head1 ENVIRONMENT VARIABLES
407              
408             =head2 GITHOOKS_SKIP
409              
410             Comma separated list of hooks to skip. A warning is issued for each hook that
411             would otherwise be triggered.
412              
413             GITHOOKS_SKIP=pre-commit,update
414              
415             =head2 GITHOOKS_DISABLE
416              
417             Works similarly to C<GITHOOKS_SKIP>, but it skips all the possible hooks. Set
418             it to a true value, e.g. 1.
419              
420             GITHOOKS_DISABLE=1
421              
422             =head2 GITHOOKSRC
423              
424             Contains path to a custom configuration file, see "Configuration file
425             locations" above.
426              
427             =head2 GITHOOKSRC_FORCE
428              
429             Similar to C<GITHOOKSRC> but with a higher priority. See "Configuration file
430             locations" above.
431              
432              
433             =head1 FUNCTIONS
434              
435             =head2 run()
436              
437             Run the specified hook.
438              
439             App::GitHooks::run(
440             name => $name,
441             arguments => \@arguments,
442             );
443              
444             Arguments:
445              
446             =over 4
447              
448             =item * name I<(mandatory)>
449              
450             The name of the git hook calling this class. See the "VALID GIT HOOK NAMES"
451             section for acceptable values.
452              
453             =item * arguments I<(optional)>
454              
455             An arrayref of arguments passed originally to the git hook.
456              
457             =item * exit I<(optional, default 1)>
458              
459             Indicate whether the method should exit (1) or simply return the exit status
460             without actually exiting (0).
461              
462             =back
463              
464             =cut
465              
466             sub run
467             {
468 25     25 1 1439171 my ( $class, %args ) = @_;
469 25         77 my $name = delete( $args{'name'} );
470 25         56 my $arguments = delete( $args{'arguments'} );
471 25   100     184 my $exit = delete( $args{'exit'} ) // 1;
472              
473             my $exit_code =
474             try
475             {
476 25 100   25   1556 croak 'Invalid argument(s): ' . join( ', ', keys %args )
477             if scalar( keys %args ) != 0;
478              
479             # Clean up hook name in case we were passed a file path.
480 24         694 $name = File::Basename::fileparse( $name );
481              
482             # Validate hook name.
483 24 50       114 croak 'A hook name must be passed'
484             if !defined( $name );
485             croak "Invalid hook name $name"
486 24 50       71 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  414         495  
487              
488 24 100       84 if (my $env_var = _should_skip( $name )) {
489 5         459 carp "Hook $name skipped because of $env_var";
490 5         2059 return $HOOK_EXIT_SUCCESS;
491             }
492              
493             # Validate arguments.
494 19 50       74 croak 'Unknown argument(s): ' . join( ', ', keys %args )
495             if scalar( keys %args ) != 0;
496              
497             # Load the hook class.
498 19         73 my $hook_class = "App::GitHooks::Hook::" . _to_camelcase( $name );
499 19         122 Class::Load::load_class( $hook_class );
500              
501             # Create a new App instance to hold the various data.
502 19         822 my $self = $class->new(
503             arguments => $arguments,
504             name => $name,
505             );
506              
507             # Force the output to match the terminal encoding.
508 19         49 my $terminal = $self->get_terminal();
509 19         78 my $terminal_encoding = $terminal->get_encoding();
510 19 50       96 binmode( STDOUT, "encoding($terminal_encoding)" )
511             if $terminal->is_utf8();
512              
513             # Run the hook.
514 19         147 my $hook_exit_code = $hook_class->run(
515             app => $self,
516             );
517 12 50       52 croak "$hook_class ran successfully but did not return an exit code."
518             if !defined( $hook_exit_code );
519              
520 12         204 return $hook_exit_code;
521             }
522             catch
523             {
524 1     1   92 chomp( $_ );
525 1         60 print STDERR "Error detected in hook: >$_<.\n";
526 1         8 return $HOOK_EXIT_FAILURE;
527 25         399 };
528              
529 18 100       702 if ( $exit )
530             {
531 9         387 exit( $exit_code );
532             }
533             else
534             {
535 9         40 return $exit_code;
536             }
537             }
538              
539              
540             =head1 METHODS
541              
542             =head2 new()
543              
544             Create a new C<App::GitHooks> object.
545              
546             my $app = App::GitHooks->new(
547             name => $name,
548             arguments => \@arguments,
549             );
550              
551             Arguments:
552              
553             =over 4
554              
555             =item * name I<(mandatory)>
556              
557             The name of the git hook calling this class. See the "VALID GIT HOOK NAMES"
558             section for acceptable values.
559              
560             =item * arguments I<(optional)>
561              
562             An arrayref of arguments passed originally to the git hook.
563              
564             =back
565              
566             =cut
567              
568             sub new
569             {
570 50     50 1 30276 my ( $class, %args ) = @_;
571 50         149 my $name = delete( $args{'name'});
572 50         133 my $arguments = delete( $args{'arguments'} );
573              
574             # Defaults.
575 50 100       268 $arguments = []
576             if !defined( $arguments );
577              
578             # Check arguments.
579 50 100       341 croak "The 'argument' parameter must be an arrayref"
580             if !Data::Validate::Type::is_arrayref( $arguments );
581 49 100       1416 croak "The argument 'name' is mandatory"
582             if !defined( $name );
583             croak "Invalid hook name $name"
584 48 100       227 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  818         1214  
585 47 50       229 croak 'The following argument(s) are not valid: ' . join( ', ', keys %args )
586             if scalar( keys %args ) != 0;
587              
588             # Create object.
589 47         545 my $self = bless(
590             {
591             plugins => undef,
592             force_non_interactive => 0,
593             terminal => App::GitHooks::Terminal->new(),
594             arguments => $arguments,
595             hook_name => $name,
596             repository => undef,
597             use_colors => 1,
598             },
599             $class,
600             );
601              
602             # Look up testing overrides.
603 47         222 my $config = $self->get_config();
604              
605 46         165 my $force_use_color = $config->get( 'testing', 'force_use_colors' );
606 46 100       265 $self->use_colors( $force_use_color )
607             if defined( $force_use_color );
608              
609 46         142 my $force_is_utf8 = $config->get( 'testing', 'force_is_utf8' );
610 46 100       187 $self->get_terminal()->is_utf8( $force_is_utf8 )
611             if defined( $force_is_utf8 );
612              
613 46         336 return $self;
614             }
615              
616              
617             =head2 clone()
618              
619             Clone the current object and override its properties with the arguments
620             specified.
621              
622             my $cloned_app = $app->clone(
623             name => $hook_name, # optional
624             );
625              
626             =over 4
627              
628             =item * name I<(optional)>
629              
630             The name of the invoking hook.
631              
632             =back
633              
634             =cut
635              
636             sub clone
637             {
638 4     4 1 3251 my ( $self, %args ) = @_;
639 4         11 my $name = delete( $args{'name'} );
640 4 100       48 croak 'Invalid argument(s): ' . join( ', ', keys %args )
641             if scalar( keys %args ) != 0;
642              
643             # Clone the object.
644 3         333 my $cloned_app = Storable::dclone( $self );
645              
646             # Overrides.
647 3 100       12 if ( defined( $name ) )
648             {
649             croak "Invalid hook name $name"
650 2 100       7 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  34         82  
651              
652 1         3 $cloned_app->{'hook_name'} = $name;
653             }
654              
655 2         13 return $cloned_app;
656             }
657              
658              
659             =head2 get_hook_plugins()
660              
661             Return an arrayref of all the plugins installed and available for a specific
662             git hook on the current system.
663              
664             my $plugins = $app->get_hook_plugins(
665             $hook_name
666             );
667              
668             Arguments:
669              
670             =over 4
671              
672             =item * $hook_name
673              
674             The name of the git hook for which to find available plugins.
675              
676             =back
677              
678             =cut
679              
680             sub get_hook_plugins
681             {
682 33     33 1 88 my ( $self, $hook_name ) = @_;
683              
684             # Check parameters.
685 33 50       120 croak "A git hook name is required"
686             if !defined( $hook_name );
687              
688             # Handle both - and _ in the hook name.
689 33         207 $hook_name =~ s/-/_/g;
690              
691             # Searching for plugins is expensive, so we cache it here.
692             $self->{'plugins'} = $self->get_all_plugins()
693 33 100       187 if !defined( $self->{'plugins'} );
694              
695 33   100     215 return $self->{'plugins'}->{ $hook_name } // [];
696             }
697              
698              
699             =head2 get_all_plugins()
700              
701             Return a hashref of the plugins available for every git hook.
702              
703             my $all_plugins = $self->get_all_plugins();
704              
705             =cut
706              
707             sub get_all_plugins
708             {
709 19     19 1 31 my ( $self ) = @_;
710 19         49 my $config = $self->get_config();
711              
712             # Find all available plugins regardless of the desired target hook, using
713             # Module::Pluggable.
714 19         177 my @discovered_plugins = __PACKAGE__->_search_plugins();
715              
716             # Warn about deprecated 'limit_plugins' config option.
717 19         13216 my $limit_plugins = $config->get( 'testing', 'limit_plugins' );
718 19 50       84 if ( defined( $limit_plugins ) )
719             {
720 0         0 carp "The configuration option 'limit_plugins' under the [testing] section "
721             . "is deprecated, please switch to using 'force_plugins' under the general "
722             . "configuration section as soon as possible";
723             }
724              
725             # If the environment restricts the list of plugins to run, we use that.
726             # Otherwise, we exclude test plugins.
727 19   66     65 my $force_plugins = $config->get( '_', 'force_plugins' )
      100        
728             // $limit_plugins
729             // '';
730 19         48 my @plugins = ();
731 19 100       155 if ( $force_plugins =~ /\w/ )
732             {
733             my %forced_plugins =
734 18         68 map { $_ => 1 }
735             # Prepend App::GitHooks::Plugin:: to the plugin name if omitted.
736 18 100       246 map { $_ =~ /^App/ ? $_ : "App::GitHooks::Plugin::$_" }
  18         122  
737             # Split the comma-separated list.
738             split( /(?:\s+|\s*,\s*)/, $force_plugins );
739              
740 18         59 foreach my $plugin ( @discovered_plugins )
741             {
742             # Only add plugins listed in the config file.
743 38 100       105 next if !$forced_plugins{ $plugin };
744              
745 18         37 push( @plugins, $plugin );
746 18         41 delete( $forced_plugins{ $plugin } );
747             }
748              
749             # If plugins listed in the config file are not found on the system, don't
750             # continue.
751 18 50       84 if ( scalar( keys %forced_plugins ) != 0 )
752             {
753 0         0 croak sprintf(
754             "The following plugins must be installed on your system, per the "
755             . "'force_plugins' directive in your githooksrc config file: %s",
756             join( ', ', keys %forced_plugins ),
757             );
758             }
759             }
760             else
761             {
762 1         4 foreach my $plugin ( @discovered_plugins )
763             {
764 2 50       8 next if $plugin =~ /^\QApp::GitHooks::Plugin::Test::\E/x;
765 0         0 push( @plugins, $plugin );
766             }
767             }
768             #print STDERR Dumper( \@plugins );
769              
770             # Parse each plugin to find out which hook(s) they apply to.
771 19         43 my $all_plugins = {};
772 19         50 foreach my $plugin ( @plugins )
773             {
774             # Load the plugin class.
775 18         87 Class::Load::load_class( $plugin );
776              
777             # Store the list of plugins available for each hook.
778 18         1353 my $hooks_declared;
779 18         29 foreach my $hook ( @{ $App::GitHooks::Plugin::SUPPORTED_SUBS } )
  18         51  
780             {
781 324 100       1186 next if !$plugin->can( 'run_' . $hook );
782 290         262 $hooks_declared = 1;
783              
784 290   50     1077 $all_plugins->{ $hook } //= [];
785 290         226 push( @{ $all_plugins->{ $hook } }, $plugin );
  290         474  
786             }
787              
788             # Alert if the plugin didn't declare any hook handling subroutines -
789             # that's probably the sign of a typo in a subroutine name.
790 18 50       115 carp "The plugin $plugin does not declare any hook handling subroutines, check for typos in sub names?"
791             if !$hooks_declared;
792             }
793              
794 19         74 return $all_plugins;
795             }
796              
797              
798             =head2 get_config()
799              
800             Retrieve the configuration information for the current project.
801              
802             my $config = $app->get_config();
803              
804             =cut
805              
806             sub get_config
807             {
808 144     144 1 281 my ( $self ) = @_;
809              
810 144 100       650 if ( !defined( $self->{'config'} ) )
811             {
812 47         83 my $config_file;
813             my $config_source;
814             # For testing purposes, provide a way to enforce a specific .githooksrc
815             # file regardless of how anything else is set up on the machine.
816 47 100 66     1172 if ( defined( $ENV{'GITHOOKSRC_FORCE'} ) && ( -e $ENV{'GITHOOKSRC_FORCE'} ) )
    50 0        
    0 0        
    0          
817             {
818 25         64 $config_source = 'GITHOOKSRC_FORCE environment variable';
819 25         61 $config_file = $ENV{'GITHOOKSRC_FORCE'};
820             }
821             # First, use repository-specific githooksrc files.
822             elsif ( -e '.githooksrc' )
823             {
824 22         52 $config_source = '.githooksrc at the root of the repository';
825 22         43 $config_file = '.githooksrc';
826             }
827             # Fall back on the GITHOOKSRC variable.
828             elsif ( defined( $ENV{'GITHOOKSRC'} ) && ( -e $ENV{'GITHOOKSRC'} ) )
829             {
830 0         0 $config_source = 'GITHOOKSRC environment variable';
831 0         0 $config_file = $ENV{'GITHOOKSRC'};
832             }
833             # Fall back on the home directory of the user.
834             elsif ( defined( $ENV{'HOME'} ) && ( -e $ENV{'HOME'} . '/.githooksrc' ) )
835             {
836 0         0 $config_source = '.githooksrc in the home directory';
837 0         0 $config_file = $ENV{'HOME'} . '/.githooksrc';
838             }
839              
840 47 50       613 $self->{'config'} = App::GitHooks::Config->new(
841             defined( $config_file )
842             ? ( file => $config_file, source => $config_source )
843             : (),
844             );
845             }
846              
847             # Enforce the specifying of min version of App::GitHooks
848 144         1814 my $min_version = $self->{'config'}->get('_','min_app_githooks_version');
849 144 100 100     462 croak "Requires at least App::Githooks version $min_version, you have version $VERSION"
850             if $min_version && $min_version gt $VERSION;
851              
852 143         410 return $self->{'config'};
853             }
854              
855              
856             =head2 force_non_interactive()
857              
858             By default C<App::GitHooks> detects whether it is running in interactive mode,
859             but this allows forcing it to run in non-interactive mode.
860              
861             # Retrieve the current setting.
862             my $force_non_interactive = $app->force_non_interactive();
863              
864             # Force non-interactive mode.
865             $app->force_non_interactive( 1 );
866              
867             # Go back to the default behavior of detecting the current mode.
868             $app->force_non_interactive( 0 );
869              
870             =cut
871              
872             sub force_non_interactive
873             {
874 6     6 1 116 my ( $self, $value ) = @_;
875              
876 6 100       17 if ( defined( $value ) )
877             {
878 3 100       16 if ( $value =~ /^(?:0|1)$/ )
879             {
880 2         6 $self->{'force_non_interactive'} = $value;
881             }
882             else
883             {
884 1         36 croak 'Invalid argument';
885             }
886             }
887              
888 5         21 return $self->{'force_non_interactive'};
889             }
890              
891              
892             =head2 get_failure_character()
893              
894             Return a character to use to indicate a failure.
895              
896             my $failure_character = $app->get_failure_character()
897              
898             =cut
899              
900             sub get_failure_character
901             {
902 8     8 1 22 my ( $self ) = @_;
903              
904 8 50       50 return $self->get_terminal()->is_utf8()
905             ? "\x{00D7}"
906             : "x";
907             }
908              
909              
910             =head2 get_success_character()
911              
912             Return a character to use to indicate a success.
913              
914             my $success_character = $app->get_success_character()
915              
916             =cut
917              
918             sub get_success_character
919             {
920 3     3 1 7 my ( $self ) = @_;
921              
922 3 50       10 return $self->get_terminal()->is_utf8()
923             ? "\x{2713}"
924             : "o";
925             }
926              
927              
928             =head2 get_warning_character()
929              
930             Return a character to use to indicate a warning.
931              
932             my $warning_character = $app->get_warning_character()
933              
934             =cut
935              
936             sub get_warning_character
937             {
938 1     1 1 2 my ( $self ) = @_;
939              
940 1 50       193 return $self->get_terminal()->is_utf8()
941             ? "\x{26A0}"
942             : "!";
943             }
944              
945              
946             =head2 get_staged_changes()
947              
948             Return a C<App::GitHooks::StagedChanges> object corresponding to the changes
949             staged in the current project.
950              
951             my $staged_changes = $app->get_staged_changes();
952              
953             =cut
954              
955             sub get_staged_changes
956             {
957 14     14 1 72 my ( $self ) = @_;
958              
959 14 50       58 if ( !defined( $self->{'staged_changes'} ) )
960             {
961 14         114 $self->{'staged_changes'} = App::GitHooks::StagedChanges->new(
962             app => $self,
963             );
964             }
965              
966 14         70 return $self->{'staged_changes'};
967             }
968              
969              
970             =head2 use_colors()
971              
972             Allows disabling the use of colors in C<App::GitHooks>'s output.
973              
974             # Retrieve the current setting.
975             my $use_colors = $app->use_colors();
976              
977             # Disable colors in the output.
978             $app->use_colors( 0 );
979              
980             =cut
981              
982             sub use_colors
983             {
984 45     45 1 113 my ( $self, $value ) = @_;
985              
986 45 100       157 if ( defined( $value ) )
987             {
988 17         44 $self->{'use_colors'} = $value;
989             }
990              
991 45         1548 return $self->{'use_colors'};
992             }
993              
994              
995             =head1 ACCESSORS
996              
997             =head2 get_repository()
998              
999             Return the underlying C<Git::Repository> object for the current project.
1000              
1001             my $repository = $app->get_repository();
1002              
1003             =cut
1004              
1005             sub get_repository
1006             {
1007 46     46 1 96 my ( $self ) = @_;
1008              
1009 46   66     346 $self->{'repository'} //= Git::Repository->new();
1010              
1011 46         948661 return $self->{'repository'};
1012             }
1013              
1014              
1015             =head2 get_remote_name()
1016              
1017             Get the name of the repository.
1018              
1019             my $remote_name = $app->get_remote_name();
1020              
1021             =cut
1022              
1023             sub get_remote_name
1024             {
1025 0     0 1 0 my ( $app ) = @_;
1026 0         0 my $repository = $app->get_repository();
1027              
1028             # Retrieve the remote path.
1029 0   0     0 my $remote = $repository->run( qw( config --get remote.origin.url ) ) // '';
1030              
1031             # Extract the remote name.
1032 0         0 my ( $remote_name ) = ( $remote =~ /\/(.*?)\.git$/i );
1033 0   0     0 $remote_name //= '(no remote found)';
1034              
1035 0         0 return $remote_name;
1036             }
1037              
1038              
1039             =head2 get_hook_name
1040              
1041             Return the name of the git hook that called the current instance.
1042              
1043             my $hook_name = $app->get_hook_name();
1044              
1045             =cut
1046              
1047             sub get_hook_name
1048             {
1049 21     21 1 1283 my ( $self ) = @_;
1050              
1051 21         125 return $self->{'hook_name'};
1052             }
1053              
1054              
1055             =head2 get_command_line_arguments()
1056              
1057             Return the arguments passed originally to the git hook.
1058              
1059             my $command_line_arguments = $app->get_command_line_arguments();
1060              
1061             =cut
1062              
1063             sub get_command_line_arguments
1064             {
1065 2     2 1 4 my ( $self ) = @_;
1066              
1067 2   50     13 return $self->{'arguments'} // [];
1068             }
1069              
1070              
1071             =head2 get_terminal()
1072              
1073             Return the C<App::GitHooks::Terminal> object associated with the current
1074             instance.
1075              
1076             my $terminal = $app->get_terminal();
1077              
1078             =cut
1079              
1080             sub get_terminal
1081             {
1082 88     88 1 141 my ( $self ) = @_;
1083              
1084 88         525 return $self->{'terminal'};
1085             }
1086              
1087              
1088             =head1 DISPLAY METHODS
1089              
1090             =head2 wrap()
1091              
1092             Format information while respecting the format width and indentation.
1093              
1094             my $string = $app->wrap( $information, $indent );
1095              
1096             =cut
1097              
1098             sub wrap
1099             {
1100 23     23 1 68 my ( $self, $information, $indent ) = @_;
1101 23   100     202 $indent //= '';
1102              
1103             return
1104 23 50       83 if !defined( $information );
1105              
1106 23         130 my $terminal_width = $self->get_terminal()->get_width();
1107 23 50       91 if ( defined( $terminal_width ) )
1108             {
1109 0         0 local $Text::Wrap::columns = $terminal_width; ## no critic (Variables::ProhibitPackageVars)
1110              
1111 0         0 return Text::Wrap::wrap(
1112             $indent,
1113             $indent,
1114             $information,
1115             );
1116             }
1117             else
1118             {
1119              
1120             return join(
1121             "\n",
1122             map
1123 23 100 66     304 { defined( $_ ) && $_ ne '' ? $indent . $_ : $_ } # Don't indent blank lines.
  44         762  
1124             split( /\n/, $information, -1 ) # Keep trailing \n's.
1125             );
1126             }
1127             }
1128              
1129              
1130             =head2 color()
1131              
1132             Print text with colors.
1133              
1134             $app->color( $color, $text );
1135              
1136             =cut
1137              
1138             sub color
1139             {
1140 28     28 1 103 my ( $self, $color, $string ) = @_;
1141              
1142 28 50       136 return $self->use_colors()
1143             ? Term::ANSIColor::colored( [ $color ], $string )
1144             : $string;
1145             }
1146              
1147              
1148             =head1 PRIVATE FUNCTIONS
1149              
1150             =head2 _to_camelcase()
1151              
1152             Convert a dash-separated string to camelcase.
1153              
1154             my $camelcase_string = App::GitHooks::_to_camelcase( $string );
1155              
1156             This function is useful to convert git hook names (commit-msg) to module names
1157             (CommitMsg).
1158              
1159             =cut
1160              
1161             sub _to_camelcase
1162             {
1163 19     19   45 my ( $name ) = @_;
1164              
1165 19         201 $name =~ s/-(.)/\U$1/g;
1166 19         71 $name = ucfirst( $name );
1167              
1168 19         70 return $name;
1169             }
1170              
1171              
1172             =head2 _should_skip()
1173              
1174             See the environment variables GITHOOKS_SKIP and GITHOOKS_DISABLE above. This
1175             function returns the variable name that would be the reason to skip the given
1176             hook, or nothing.
1177              
1178             return if _should_skip( $name );
1179              
1180             =cut
1181              
1182             sub _should_skip
1183             {
1184 24     24   52 my ( $name ) = @_;
1185             return unless exists $ENV{'GITHOOKS_SKIP'}
1186 24 100 66     201 || exists $ENV{'GITHOOKS_DISABLE'};
1187              
1188 5 100       10 return 'GITHOOKS_DISABLE' if $ENV{'GITHOOKS_DISABLE'};
1189              
1190 4         4 my %skip;
1191 4         14 @skip{ split /,/, $ENV{'GITHOOKS_SKIP'} } = ();
1192 4   50     22 return exists $skip{ $name } && 'GITHOOKS_SKIP';
1193             }
1194              
1195              
1196             =head1 NOTES
1197              
1198             =head2 Manual installation
1199              
1200             Symlink your git hooks under .git/hooks to a file with the following content:
1201              
1202             #!/usr/bin/env perl
1203              
1204             use strict;
1205             use warnings;
1206              
1207             use App::GitHooks;
1208              
1209             App::GitHooks->run(
1210             name => $0,
1211             arguments => \@ARGV,
1212             );
1213              
1214             All you need to do then is install the plugins you are interested in!
1215              
1216             This distribution also includes a C<hooks/> directory that you can symlink /
1217             copy to C<.git/hooks/> instead , to get all the hooks set up properly in one
1218             swoop.
1219              
1220             Important: adjust C</usr/bin/env perl> as needed, if that line is not a valid
1221             interpreter, your git actions will fail with C<error: cannot run
1222             .git/hooks/[hook name]: No such file or directory>.
1223              
1224              
1225             =head1 BUGS
1226              
1227             Please report any bugs or feature requests through the web interface at
1228             L<https://github.com/guillaumeaubert/App-GitHooks/issues/new>.
1229             I will be notified, and then you'll automatically be notified of progress on
1230             your bug as I make changes.
1231              
1232              
1233             =head1 SUPPORT
1234              
1235             You can find documentation for this module with the perldoc command.
1236              
1237             perldoc App::GitHooks
1238              
1239              
1240             You can also look for information at:
1241              
1242             =over
1243              
1244             =item * GitHub's request tracker
1245              
1246             L<https://github.com/guillaumeaubert/App-GitHooks/issues>
1247              
1248             =item * AnnoCPAN: Annotated CPAN documentation
1249              
1250             L<http://annocpan.org/dist/app-githooks>
1251              
1252             =item * CPAN Ratings
1253              
1254             L<http://cpanratings.perl.org/d/app-githooks>
1255              
1256             =item * MetaCPAN
1257              
1258             L<https://metacpan.org/release/App-GitHooks>
1259              
1260             =back
1261              
1262              
1263             =head1 AUTHOR
1264              
1265             L<Guillaume Aubert|https://metacpan.org/author/AUBERTG>,
1266             C<< <aubertg at cpan.org> >>.
1267              
1268              
1269             =head1 COPYRIGHT & LICENSE
1270              
1271             Copyright 2013-2017 Guillaume Aubert.
1272              
1273             This code is free software; you can redistribute it and/or modify it under the
1274             same terms as Perl 5 itself.
1275              
1276             This program is distributed in the hope that it will be useful, but WITHOUT ANY
1277             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
1278             PARTICULAR PURPOSE. See the LICENSE file for more details.
1279              
1280             =cut
1281              
1282             1;