File Coverage

blib/lib/App/GitHooks.pm
Criterion Covered Total %
statement 196 211 92.8
branch 64 88 72.7
condition 18 34 52.9
subroutine 40 41 97.5
pod 19 19 100.0
total 337 393 85.7


line stmt bran cond sub pod time code
1             package App::GitHooks;
2              
3 29     29   25753 use strict;
  29         47  
  29         664  
4 29     29   94 use warnings;
  29         25  
  29         941  
5              
6             # Unbuffer output to display it as quickly as possible.
7             local $| = 1;
8              
9             # External dependencies.
10 29     29   89 use Carp qw( carp croak );
  29         28  
  29         1273  
11 29     29   11561 use Class::Load qw();
  29         401169  
  29         593  
12 29     29   12049 use Config::Tiny qw();
  29         21561  
  29         494  
13 29     29   12738 use Data::Validate::Type qw();
  29         156820  
  29         652  
14 29     29   16233 use Data::Dumper qw( Dumper );
  29         153960  
  29         1677  
15 29     29   148 use File::Basename qw();
  29         29  
  29         343  
16 29     29   11887 use Git::Repository qw();
  29         725742  
  29         747  
17             use Module::Pluggable
18 29         155 require => 1,
19 29     29   12041 sub_name => '_search_plugins';
  29         182742  
20 29     29   17657 use Term::ANSIColor qw();
  29         141996  
  29         732  
21 29     29   11983 use Text::Wrap qw();
  29         52960  
  29         590  
22 29     29   130 use Try::Tiny qw( try catch finally );
  29         31  
  29         1372  
23 29     29   15170 use Storable qw();
  29         60780  
  29         641  
24              
25             # Internal dependencies.
26 29     29   10337 use App::GitHooks::Config;
  29         46  
  29         753  
27 29     29   8359 use App::GitHooks::Constants qw( :HOOK_EXIT_CODES );
  29         45  
  29         3235  
28 29     29   8838 use App::GitHooks::Plugin;
  29         52  
  29         644  
29 29     29   9847 use App::GitHooks::StagedChanges;
  29         54  
  29         807  
30 29     29   10266 use App::GitHooks::Terminal;
  29         50  
  29         50116  
31              
32              
33             =head1 NAME
34              
35             App::GitHooks - Extensible plugins system for git hooks.
36              
37              
38             =head1 VERSION
39              
40             Version 1.8.0
41              
42             =cut
43              
44             our $VERSION = '1.8.0';
45              
46              
47             =head1 DESCRIPTION
48              
49             C 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 hook checks before
52             the commit message can be entered:
53              
54             =begin html
55              
56            
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            
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 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 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
178              
179             Prevent committing code with #NOCOMMIT mentions.
180              
181             =item * L
182              
183             Prevent commits in a production environment.
184              
185             =item * L
186              
187             Find out when someone uses --no-verify and append the pre-commit checks to the
188             commit message.
189              
190             =item * L
191              
192             Force running a specific tool at regular intervals.
193              
194             =item * L
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
200              
201             Verify that Perl files compile without errors.
202              
203             =item * L
204              
205             Verify that all changes and addition to the Perl files pass PerlCritic checks.
206              
207             =item * L
208              
209             Enforce a specific Perl interpreter on the first line of Perl files.
210              
211             =item * L
212              
213             Verify that the syntax of PgBouncer auth files is correct.
214              
215             =item * L
216              
217             Derive a ticket ID from the branch name and prepend it to the commit-message.
218              
219             =item * L
220              
221             Require a commit message.
222              
223             =item * L
224              
225             Verify that staged Ruby files compile.
226              
227             =item * L
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
239              
240             Verify that staged Ruby files compile.
241              
242             =item * L
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 uses L, 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 above) and plugin specific configuration
264             options which are located in distinct sections (such as C in the
265             C<[section_1]> section).
266              
267              
268             =head2 Configuration file locations
269              
270             L 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 to its path.
279              
280             Note that you should normally use C. 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 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
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 or C.
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.
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             =back
366              
367              
368             =head2 Testing-specific options
369              
370             =over 4
371              
372             =item * limit_plugins
373              
374             Deprecated. Use C instead.
375              
376             =item * force_interactive
377              
378             Force the application to consider that the terminal is interactive (`1`) or
379             non-interactive (`0`) independently of whether the actual STDOUT is interactive
380             or not.
381              
382             =item * force_use_colors
383              
384             Force the output to use colors (`1`) or to not use colors (`0`) independently
385             of the ability of STDOUT to display colors.
386              
387             =item * force_is_utf8
388              
389             Allows the output to use utf-8 characters (`1`) or not (`0`), independently of
390             whether the output declares supporting utf-8.
391              
392             =item * commit_msg_no_edit
393              
394             Allows skipping the loop to edit the message when the commit message checks
395             failed.
396              
397             =back
398              
399              
400             =head1 FUNCTIONS
401              
402             =head2 run()
403              
404             Run the specified hook.
405              
406             App::GitHooks::run(
407             name => $name,
408             arguments => \@arguments,
409             );
410              
411             Arguments:
412              
413             =over 4
414              
415             =item * name I<(mandatory)>
416              
417             The name of the git hook calling this class. See the "VALID GIT HOOK NAMES"
418             section for acceptable values.
419              
420             =item * arguments I<(optional)>
421              
422             An arrayref of arguments passed originally to the git hook.
423              
424             =item * exit I<(optional, default 1)>
425              
426             Indicate whether the method should exit (1) or simply return the exit status
427             without actually exiting (0).
428              
429             =back
430              
431             =cut
432              
433             sub run
434             {
435 19     19 1 1151615 my ( $class, %args ) = @_;
436 19         48 my $name = delete( $args{'name'} );
437 19         38 my $arguments = delete( $args{'arguments'} );
438 19   100     127 my $exit = delete( $args{'exit'} ) // 1;
439              
440             my $exit_code =
441             try
442             {
443 19 100   19   851 croak 'Invalid argument(s): ' . join( ', ', keys %args )
444             if scalar( keys %args ) != 0;
445              
446             # Clean up hook name in case we were passed a file path.
447 18         448 $name = File::Basename::fileparse( $name );
448              
449             # Validate hook name.
450 18 50       73 croak 'A hook name must be passed'
451             if !defined( $name );
452             croak "Invalid hook name $name"
453 18 50       46 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  306         315  
454              
455             # Validate arguments.
456 18 50       55 croak 'Unknown argument(s): ' . join( ', ', keys %args )
457             if scalar( keys %args ) != 0;
458              
459             # Load the hook class.
460 18         67 my $hook_class = "App::GitHooks::Hook::" . _to_camelcase( $name );
461 18         77 Class::Load::load_class( $hook_class );
462              
463             # Create a new App instance to hold the various data.
464 18         479 my $self = $class->new(
465             arguments => $arguments,
466             name => $name,
467             );
468              
469             # Force the output to match the terminal encoding.
470 18         34 my $terminal = $self->get_terminal();
471 18         56 my $terminal_encoding = $terminal->get_encoding();
472 18 50       64 binmode( STDOUT, "encoding($terminal_encoding)" )
473             if $terminal->is_utf8();
474              
475             # Run the hook.
476 18         116 my $hook_exit_code = $hook_class->run(
477             app => $self,
478             );
479 11 50       45 croak "$hook_class ran successfully but did not return an exit code."
480             if !defined( $hook_exit_code );
481              
482 11         141 return $hook_exit_code;
483             }
484             catch
485             {
486 1     1   53 chomp( $_ );
487 1         45 print STDERR "Error detected in hook: >$_<.\n";
488 1         4 return $HOOK_EXIT_FAILURE;
489 19         206 };
490              
491 12 100       432 if ( $exit )
492             {
493 9         349 exit( $exit_code );
494             }
495             else
496             {
497 3         14 return $exit_code;
498             }
499             }
500              
501              
502             =head1 METHODS
503              
504             =head2 new()
505              
506             Create a new C object.
507              
508             my $app = App::GitHooks->new(
509             name => $name,
510             arguments => \@arguments,
511             );
512              
513             Arguments:
514              
515             =over 4
516              
517             =item * name I<(mandatory)>
518              
519             The name of the git hook calling this class. See the "VALID GIT HOOK NAMES"
520             section for acceptable values.
521              
522             =item * arguments I<(optional)>
523              
524             An arrayref of arguments passed originally to the git hook.
525              
526             =back
527              
528             =cut
529              
530             sub new
531             {
532 46     46 1 19060 my ( $class, %args ) = @_;
533 46         103 my $name = delete( $args{'name'});
534 46         112 my $arguments = delete( $args{'arguments'} );
535              
536             # Defaults.
537 46 100       223 $arguments = []
538             if !defined( $arguments );
539              
540             # Check arguments.
541 46 100       277 croak "The 'argument' parameter must be an arrayref"
542             if !Data::Validate::Type::is_arrayref( $arguments );
543 45 100       1324 croak "The argument 'name' is mandatory"
544             if !defined( $name );
545             croak "Invalid hook name $name"
546 44 100       96 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  749         779  
547 43 50       126 croak 'The following argument(s) are not valid: ' . join( ', ', keys %args )
548             if scalar( keys %args ) != 0;
549              
550             # Create object.
551 43         322 my $self = bless(
552             {
553             plugins => undef,
554             force_non_interactive => 0,
555             terminal => App::GitHooks::Terminal->new(),
556             arguments => $arguments,
557             hook_name => $name,
558             repository => undef,
559             use_colors => 1,
560             },
561             $class,
562             );
563              
564             # Look up testing overrides.
565 43         129 my $config = $self->get_config();
566              
567 43         148 my $force_use_color = $config->get( 'testing', 'force_use_colors' );
568 43 100       128 $self->use_colors( $force_use_color )
569             if defined( $force_use_color );
570              
571 43         92 my $force_is_utf8 = $config->get( 'testing', 'force_is_utf8' );
572 43 100       126 $self->get_terminal()->is_utf8( $force_is_utf8 )
573             if defined( $force_is_utf8 );
574              
575 43         128 return $self;
576             }
577              
578              
579             =head2 clone()
580              
581             Clone the current object and override its properties with the arguments
582             specified.
583              
584             my $cloned_app = $app->clone(
585             name => $hook_name, # optional
586             );
587              
588             =over 4
589              
590             =item * name I<(optional)>
591              
592             The name of the invoking hook.
593              
594             =back
595              
596             =cut
597              
598             sub clone
599             {
600 4     4 1 1941 my ( $self, %args ) = @_;
601 4         6 my $name = delete( $args{'name'} );
602 4 100       31 croak 'Invalid argument(s): ' . join( ', ', keys %args )
603             if scalar( keys %args ) != 0;
604              
605             # Clone the object.
606 3         216 my $cloned_app = Storable::dclone( $self );
607              
608             # Overrides.
609 3 100       7 if ( defined( $name ) )
610             {
611             croak "Invalid hook name $name"
612 2 100       5 if scalar( grep { $_ eq $name } @$HOOK_NAMES ) == 0;
  34         41  
613              
614 1         2 $cloned_app->{'hook_name'} = $name;
615             }
616              
617 2         8 return $cloned_app;
618             }
619              
620              
621             =head2 get_hook_plugins()
622              
623             Return an arrayref of all the plugins installed and available for a specific
624             git hook on the current system.
625              
626             my $plugins = $app->get_hook_plugins(
627             $hook_name
628             );
629              
630             Arguments:
631              
632             =over 4
633              
634             =item * $hook_name
635              
636             The name of the git hook for which to find available plugins.
637              
638             =back
639              
640             =cut
641              
642             sub get_hook_plugins
643             {
644 33     33 1 54 my ( $self, $hook_name ) = @_;
645              
646             # Check parameters.
647 33 50       78 croak "A git hook name is required"
648             if !defined( $hook_name );
649              
650             # Handle both - and _ in the hook name.
651 33         132 $hook_name =~ s/-/_/g;
652              
653             # Searching for plugins is expensive, so we cache it here.
654             $self->{'plugins'} = $self->get_all_plugins()
655 33 100       118 if !defined( $self->{'plugins'} );
656              
657 33   100     139 return $self->{'plugins'}->{ $hook_name } // [];
658             }
659              
660              
661             =head2 get_all_plugins()
662              
663             Return a hashref of the plugins available for every git hook.
664              
665             my $all_plugins = $self->get_all_plugins();
666              
667             =cut
668              
669             sub get_all_plugins
670             {
671 19     19 1 23 my ( $self ) = @_;
672 19         32 my $config = $self->get_config();
673              
674             # Find all available plugins regardless of the desired target hook, using
675             # Module::Pluggable.
676 19         119 my @discovered_plugins = __PACKAGE__->_search_plugins();
677              
678             # Warn about deprecated 'limit_plugins' config option.
679 19         11491 my $limit_plugins = $config->get( 'testing', 'limit_plugins' );
680 19 50       61 if ( defined( $limit_plugins ) )
681             {
682 0         0 carp "The configuration option 'limit_plugins' under the [testing] section "
683             . "is deprecated, please switch to using 'force_plugins' under the general "
684             . "configuration section as soon as possible";
685             }
686              
687             # If the environment restricts the list of plugins to run, we use that.
688             # Otherwise, we exclude test plugins.
689 19   66     51 my $force_plugins = $config->get( '_', 'force_plugins' )
      100        
690             // $limit_plugins
691             // '';
692 19         39 my @plugins = ();
693 19 100       96 if ( $force_plugins =~ /\w/ )
694             {
695             my %forced_plugins =
696 18         55 map { $_ => 1 }
697             # Prepend App::GitHooks::Plugin:: to the plugin name if omitted.
698 18 100       152 map { $_ =~ /^App/ ? $_ : "App::GitHooks::Plugin::$_" }
  18         86  
699             # Split the comma-separated list.
700             split( /(?:\s+|\s*,\s*)/, $force_plugins );
701              
702 18         42 foreach my $plugin ( @discovered_plugins )
703             {
704             # Only add plugins listed in the config file.
705 38 100       81 next if !$forced_plugins{ $plugin };
706              
707 18         31 push( @plugins, $plugin );
708 18         27 delete( $forced_plugins{ $plugin } );
709             }
710              
711             # If plugins listed in the config file are not found on the system, don't
712             # continue.
713 18 50       66 if ( scalar( keys %forced_plugins ) != 0 )
714             {
715 0         0 croak sprintf(
716             "The following plugins must be installed on your system, per the "
717             . "'force_plugins' directive in your githooksrc config file: %s",
718             join( ', ', keys %forced_plugins ),
719             );
720             }
721             }
722             else
723             {
724 1         3 foreach my $plugin ( @discovered_plugins )
725             {
726 2 50       8 next if $plugin =~ /^\QApp::GitHooks::Plugin::Test::\E/x;
727 0         0 push( @plugins, $plugin );
728             }
729             }
730             #print STDERR Dumper( \@plugins );
731              
732             # Parse each plugin to find out which hook(s) they apply to.
733 19         43 my $all_plugins = {};
734 19         35 foreach my $plugin ( @plugins )
735             {
736             # Load the plugin class.
737 18         121 Class::Load::load_class( $plugin );
738              
739             # Store the list of plugins available for each hook.
740 18         984 my $hooks_declared;
741 18         26 foreach my $hook ( @{ $App::GitHooks::Plugin::SUPPORTED_SUBS } )
  18         45  
742             {
743 324 100       1008 next if !$plugin->can( 'run_' . $hook );
744 290         196 $hooks_declared = 1;
745              
746 290   50     784 $all_plugins->{ $hook } //= [];
747 290         178 push( @{ $all_plugins->{ $hook } }, $plugin );
  290         378  
748             }
749              
750             # Alert if the plugin didn't declare any hook handling subroutines -
751             # that's probably the sign of a typo in a subroutine name.
752 18 50       52 carp "The plugin $plugin does not declare any hook handling subroutines, check for typos in sub names?"
753             if !$hooks_declared;
754             }
755              
756 19         67 return $all_plugins;
757             }
758              
759              
760             =head2 get_config()
761              
762             Retrieve the configuration information for the current project.
763              
764             my $config = $app->get_config();
765              
766             =cut
767              
768             sub get_config
769             {
770 140     140 1 206 my ( $self ) = @_;
771              
772 140 100       497 if ( !defined( $self->{'config'} ) )
773             {
774 43         53 my $config_file;
775             my $config_source;
776             # For testing purposes, provide a way to enforce a specific .githooksrc
777             # file regardless of how anything else is set up on the machine.
778 43 100 66     781 if ( defined( $ENV{'GITHOOKSRC_FORCE'} ) && ( -e $ENV{'GITHOOKSRC_FORCE'} ) )
    50 0        
    0 0        
    0          
779             {
780 22         45 $config_source = 'GITHOOKSRC_FORCE environment variable';
781 22         38 $config_file = $ENV{'GITHOOKSRC_FORCE'};
782             }
783             # First, use repository-specific githooksrc files.
784             elsif ( -e '.githooksrc' )
785             {
786 21         35 $config_source = '.githooksrc at the root of the repository';
787 21         31 $config_file = '.githooksrc';
788             }
789             # Fall back on the GITHOOKSRC variable.
790             elsif ( defined( $ENV{'GITHOOKSRC'} ) && ( -e $ENV{'GITHOOKSRC'} ) )
791             {
792 0         0 $config_source = 'GITHOOKSRC environment variable';
793 0         0 $config_file = $ENV{'GITHOOKSRC'};
794             }
795             # Fall back on the home directory of the user.
796             elsif ( defined( $ENV{'HOME'} ) && ( -e $ENV{'HOME'} . '/.githooksrc' ) )
797             {
798 0         0 $config_source = '.githooksrc in the home directory';
799 0         0 $config_file = $ENV{'HOME'} . '/.githooksrc';
800             }
801              
802 43 50       381 $self->{'config'} = App::GitHooks::Config->new(
803             defined( $config_file )
804             ? ( file => $config_file, source => $config_source )
805             : (),
806             );
807             }
808              
809 140         432 return $self->{'config'};
810             }
811              
812              
813             =head2 force_non_interactive()
814              
815             By default C detects whether it is running in interactive mode,
816             but this allows forcing it to run in non-interactive mode.
817              
818             # Retrieve the current setting.
819             my $force_non_interactive = $app->force_non_interactive();
820              
821             # Force non-interactive mode.
822             $app->force_non_interactive( 1 );
823              
824             # Go back to the default behavior of detecting the current mode.
825             $app->force_non_interactive( 0 );
826              
827             =cut
828              
829             sub force_non_interactive
830             {
831 6     6 1 73 my ( $self, $value ) = @_;
832              
833 6 100       12 if ( defined( $value ) )
834             {
835 3 100       12 if ( $value =~ /^(?:0|1)$/ )
836             {
837 2         3 $self->{'force_non_interactive'} = $value;
838             }
839             else
840             {
841 1         16 croak 'Invalid argument';
842             }
843             }
844              
845 5         16 return $self->{'force_non_interactive'};
846             }
847              
848              
849             =head2 get_failure_character()
850              
851             Return a character to use to indicate a failure.
852              
853             my $failure_character = $app->get_failure_character()
854              
855             =cut
856              
857             sub get_failure_character
858             {
859 8     8 1 20 my ( $self ) = @_;
860              
861 8 50       48 return $self->get_terminal()->is_utf8()
862             ? "\x{00D7}"
863             : "x";
864             }
865              
866              
867             =head2 get_success_character()
868              
869             Return a character to use to indicate a success.
870              
871             my $success_character = $app->get_success_character()
872              
873             =cut
874              
875             sub get_success_character
876             {
877 3     3 1 5 my ( $self ) = @_;
878              
879 3 50       14 return $self->get_terminal()->is_utf8()
880             ? "\x{2713}"
881             : "o";
882             }
883              
884              
885             =head2 get_warning_character()
886              
887             Return a character to use to indicate a warning.
888              
889             my $warning_character = $app->get_warning_character()
890              
891             =cut
892              
893             sub get_warning_character
894             {
895 1     1 1 1 my ( $self ) = @_;
896              
897 1 50       3 return $self->get_terminal()->is_utf8()
898             ? "\x{26A0}"
899             : "!";
900             }
901              
902              
903             =head2 get_staged_changes()
904              
905             Return a C object corresponding to the changes
906             staged in the current project.
907              
908             my $staged_changes = $app->get_staged_changes();
909              
910             =cut
911              
912             sub get_staged_changes
913             {
914 14     14 1 22 my ( $self ) = @_;
915              
916 14 50       38 if ( !defined( $self->{'staged_changes'} ) )
917             {
918 14         76 $self->{'staged_changes'} = App::GitHooks::StagedChanges->new(
919             app => $self,
920             );
921             }
922              
923 14         48 return $self->{'staged_changes'};
924             }
925              
926              
927             =head2 use_colors()
928              
929             Allows disabling the use of colors in C's output.
930              
931             # Retrieve the current setting.
932             my $use_colors = $app->use_colors();
933              
934             # Disable colors in the output.
935             $app->use_colors( 0 );
936              
937             =cut
938              
939             sub use_colors
940             {
941 45     45 1 104 my ( $self, $value ) = @_;
942              
943 45 100       138 if ( defined( $value ) )
944             {
945 17         27 $self->{'use_colors'} = $value;
946             }
947              
948 45         266 return $self->{'use_colors'};
949             }
950              
951              
952             =head1 ACCESSORS
953              
954             =head2 get_repository()
955              
956             Return the underlying C object for the current project.
957              
958             my $repository = $app->get_repository();
959              
960             =cut
961              
962             sub get_repository
963             {
964 46     46 1 53 my ( $self ) = @_;
965              
966 46   66     209 $self->{'repository'} //= Git::Repository->new();
967              
968 46         471565 return $self->{'repository'};
969             }
970              
971              
972             =head2 get_remote_name()
973              
974             Get the name of the repository.
975              
976             my $remote_name = $app->get_remote_name();
977              
978             =cut
979              
980             sub get_remote_name
981             {
982 0     0 1 0 my ( $app ) = @_;
983 0         0 my $repository = $app->get_repository();
984              
985             # Retrieve the remote path.
986 0   0     0 my $remote = $repository->run( qw( config --get remote.origin.url ) ) // '';
987              
988             # Extract the remote name.
989 0         0 my ( $remote_name ) = ( $remote =~ /\/(.*?)\.git$/i );
990 0   0     0 $remote_name //= '(no remote found)';
991              
992 0         0 return $remote_name;
993             }
994              
995              
996             =head2 get_hook_name
997              
998             Return the name of the git hook that called the current instance.
999              
1000             my $hook_name = $app->get_hook_name();
1001              
1002             =cut
1003              
1004             sub get_hook_name
1005             {
1006 21     21 1 620 my ( $self ) = @_;
1007              
1008 21         216 return $self->{'hook_name'};
1009             }
1010              
1011              
1012             =head2 get_command_line_arguments()
1013              
1014             Return the arguments passed originally to the git hook.
1015              
1016             my $command_line_arguments = $app->get_command_line_arguments();
1017              
1018             =cut
1019              
1020             sub get_command_line_arguments
1021             {
1022 2     2 1 3 my ( $self ) = @_;
1023              
1024 2   50     6 return $self->{'arguments'} // [];
1025             }
1026              
1027              
1028             =head2 get_terminal()
1029              
1030             Return the C object associated with the current
1031             instance.
1032              
1033             my $terminal = $app->get_terminal();
1034              
1035             =cut
1036              
1037             sub get_terminal
1038             {
1039 87     87 1 111 my ( $self ) = @_;
1040              
1041 87         585 return $self->{'terminal'};
1042             }
1043              
1044              
1045             =head1 DISPLAY METHODS
1046              
1047             =head2 wrap()
1048              
1049             Format information while respecting the format width and indentation.
1050              
1051             my $string = $app->wrap( $information, $indent );
1052              
1053             =cut
1054              
1055             sub wrap
1056             {
1057 23     23 1 38 my ( $self, $information, $indent ) = @_;
1058 23   100     130 $indent //= '';
1059              
1060             return
1061 23 50       60 if !defined( $information );
1062              
1063 23         72 my $terminal_width = $self->get_terminal()->get_width();
1064 23 50       70 if ( defined( $terminal_width ) )
1065             {
1066 0         0 local $Text::Wrap::columns = $terminal_width; ## no critic (Variables::ProhibitPackageVars)
1067              
1068 0         0 return Text::Wrap::wrap(
1069             $indent,
1070             $indent,
1071             $information,
1072             );
1073             }
1074             else
1075             {
1076              
1077             return join(
1078             "\n",
1079             map
1080 23 100 66     94 { defined( $_ ) && $_ ne '' ? $indent . $_ : $_ } # Don't indent blank lines.
  44         356  
1081             split( /\n/, $information, -1 ) # Keep trailing \n's.
1082             );
1083             }
1084             }
1085              
1086              
1087             =head2 color()
1088              
1089             Print text with colors.
1090              
1091             $app->color( $color, $text );
1092              
1093             =cut
1094              
1095             sub color
1096             {
1097 28     28 1 66 my ( $self, $color, $string ) = @_;
1098              
1099 28 50       111 return $self->use_colors()
1100             ? Term::ANSIColor::colored( [ $color ], $string )
1101             : $string;
1102             }
1103              
1104              
1105             =head1 PRIVATE FUNCTIONS
1106              
1107             =head2 _to_camelcase()
1108              
1109             Convert a dash-separated string to camelcase.
1110              
1111             my $camelcase_string = App::GitHooks::_to_camelcase( $string );
1112              
1113             This function is useful to convert git hook names (commit-msg) to module names
1114             (CommitMsg).
1115              
1116             =cut
1117              
1118             sub _to_camelcase
1119             {
1120 18     18   32 my ( $name ) = @_;
1121              
1122 18         147 $name =~ s/-(.)/\U$1/g;
1123 18         51 $name = ucfirst( $name );
1124              
1125 18         50 return $name;
1126             }
1127              
1128              
1129             =head1 NOTES
1130              
1131             =head2 Manual installation
1132              
1133             Symlink your git hooks under .git/hooks to a file with the following content:
1134              
1135             #!/usr/bin/env perl
1136              
1137             use strict;
1138             use warnings;
1139              
1140             use App::GitHooks;
1141              
1142             App::GitHooks->run(
1143             name => $0,
1144             arguments => \@ARGV,
1145             );
1146              
1147             All you need to do then is install the plugins you are interested in!
1148              
1149             This distribution also includes a C directory that you can symlink /
1150             copy to C<.git/hooks/> instead , to get all the hooks set up properly in one
1151             swoop.
1152              
1153             Important: adjust C as needed, if that line is not a valid
1154             interpreter, your git actions will fail with C
1155             .git/hooks/[hook name]: No such file or directory>.
1156              
1157              
1158             =head1 BUGS
1159              
1160             Please report any bugs or feature requests through the web interface at
1161             L.
1162             I will be notified, and then you'll automatically be notified of progress on
1163             your bug as I make changes.
1164              
1165              
1166             =head1 SUPPORT
1167              
1168             You can find documentation for this module with the perldoc command.
1169              
1170             perldoc App::GitHooks
1171              
1172              
1173             You can also look for information at:
1174              
1175             =over
1176              
1177             =item * GitHub's request tracker
1178              
1179             L
1180              
1181             =item * AnnoCPAN: Annotated CPAN documentation
1182              
1183             L
1184              
1185             =item * CPAN Ratings
1186              
1187             L
1188              
1189             =item * MetaCPAN
1190              
1191             L
1192              
1193             =back
1194              
1195              
1196             =head1 AUTHOR
1197              
1198             L,
1199             C<< >>.
1200              
1201              
1202             =head1 COPYRIGHT & LICENSE
1203              
1204             Copyright 2013-2016 Guillaume Aubert.
1205              
1206             This code is free software; you can redistribute it and/or modify it under the
1207             same terms as Perl 5 itself.
1208              
1209             This program is distributed in the hope that it will be useful, but WITHOUT ANY
1210             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
1211             PARTICULAR PURPOSE. See the LICENSE file for more details.
1212              
1213             =cut
1214              
1215             1;