File Coverage

blib/lib/App/GitHooks/Plugin/ForceBranchNamePattern.pm
Criterion Covered Total %
statement 58 60 96.6
branch 13 18 72.2
condition 2 3 66.6
subroutine 8 8 100.0
pod 2 2 100.0
total 83 91 91.2


line stmt bran cond sub pod time code
1             package App::GitHooks::Plugin::ForceBranchNamePattern;
2              
3 11     11   1743658 use strict;
  11         15  
  11         234  
4 11     11   31 use warnings;
  11         15  
  11         201  
5              
6 11     11   31 use base 'App::GitHooks::Plugin';
  11         13  
  11         983  
7              
8             # External dependencies.
9 11     11   4223 use Log::Any qw($log);
  11         41706  
  11         35  
10              
11             # Internal dependencies.
12 11     11   31374 use App::GitHooks::Constants qw( :PLUGIN_RETURN_CODES );
  11         3447  
  11         1137  
13 11     11   4163 use App::GitHooks::Utils;
  11         9543  
  11         4805  
14              
15             # Uncomment to see debug information.
16             #use Log::Any::Adapter ('Stderr');
17              
18              
19             =head1 NAME
20              
21             App::GitHooks::Plugin::ForceBranchNamePattern - Require branch names to match a given pattern before they can be pushed to the origin.
22              
23              
24             =head1 DESCRIPTION
25              
26             For example, if you define in your .githooksrc file the following:
27              
28             [ForceBranchNamePattern]
29             branch_name_pattern = /^[a-zA-Z0-9]+$/
30              
31             Then a branch named C can be pushed to the origin, but not one
32             named C.
33              
34             A practical use of this plugin is making Puppet environment out of git
35             branches, since Puppet environment names must be strictly alphanumeric.
36              
37              
38             =head1 VERSION
39              
40             Version 1.1.0
41              
42             =cut
43              
44             our $VERSION = '1.1.0';
45              
46              
47             =head1 MINIMUM GIT VERSION
48              
49             This plugin relies on the pre-push hook, which is only available as of git
50             v1.8.2.
51              
52              
53             =head1 CONFIGURATION OPTIONS
54              
55             This plugin supports the following options in your C<.githooksrc> file.
56              
57             project_prefixes = OPS, DEV
58            
59             [ForceBranchNamePattern]
60             branch_name_pattern = /^[a-zA-Z0-9]+$/
61              
62              
63             =head2 project_prefixes
64              
65             Optional, a comma-separated list of project prefixes in case you want to use
66             them in the C regex.
67              
68             This setting must be added in the main section of your C<.githooksrc> file, as
69             it is used by multiple plugins.
70              
71             project_prefixes = OPS, DEV
72              
73              
74             =head2 branch_name_pattern
75              
76             A regular expression that will be used to check branch names before allowing
77             you to push them to the origin.
78              
79             This setting must be added in the C<[ForceBranchNamePattern]> section of your
80             C<.githooksrc> file.
81              
82             # Require alphanumeric branches only.
83             branch_name_pattern = /^[a-zA-Z0-9]+$/
84              
85             # Require branches to start with a JIRA ticket ID followed by an underscore.
86             branch_name_pattern = /^DEV-\d+_/
87              
88             # Require branches to start with a JIRA ticket ID followed by an underscore,
89             # but they can have an optional user prefix.
90             branch_name_pattern = /^(?:[^\/]+\/)?DEV-\d+_/
91              
92             # Re-use "project_prefixes" defined in the main section of the config.
93             branch_name_pattern = /^$project_prefixes-\d+_/
94              
95              
96             =head1 METHODS
97              
98             =head2 run_pre_push()
99              
100             Code to execute as part of the pre-push hook.
101              
102             my $plugin_return_code = App::GitHooks::Plugin::ForceBranchNamePattern->run_pre_push(
103             app => $app,
104             stdin => $stdin,
105             );
106              
107             Arguments:
108              
109             =over 4
110              
111             =item * $app I<(mandatory)>
112              
113             An C object.
114              
115             =item * $stdin I<(mandatory)>
116              
117             The content provided by git on stdin, corresponding to a list of references
118             being pushed.
119              
120             =back
121              
122             =cut
123              
124             sub run_pre_push
125             {
126 10     10 1 35918 my ( $class, %args ) = @_;
127 10         17 my $app = delete( $args{'app'} );
128 10         15 my $stdin = delete( $args{'stdin'} );
129              
130 10         26 $log->info( 'Entering ForceBranchNamePattern.' );
131              
132 10         172 my $config = $app->get_config();
133 10         56 my $repository = $app->get_repository();
134              
135             # Check if we have a branch name pattern specified in the config. If not,
136             # skip this plugin.
137 10         267308 my $branch_name_pattern = $config->get_regex( 'ForceBranchNamePattern', 'branch_name_pattern' );
138 10 50       483 if ( !defined( $branch_name_pattern ) )
139             {
140 0         0 $log->infof("No 'branch_name_pattern' specified in the [ForceBranchNamePattern] section of the config, skipping plugin.");
141 0         0 return $PLUGIN_RETURN_SKIPPED;
142             }
143              
144             # Insert valid project prefixes in the branch name verification regex.
145 10 100       47 if ( $branch_name_pattern =~ /\$project_prefixes/ )
146             {
147             # Make sure we have valid project prefixes defined in the config.
148 3         14 my $project_prefix_regex = App::GitHooks::Utils::get_project_prefix_regex( $app );
149 3 100 66     100 if ( !defined( $project_prefix_regex ) || ( $project_prefix_regex eq '' ) )
150             {
151 1         2 my $error =
152             "No 'project_prefixes' values specified, but required in the pattern " .
153             "specified by 'branch_name_pattern' in the [ForceBranchNamePattern] " .
154             "section of the config. Please fix your .githooksrc config.";
155 1         7 $log->error( $error );
156 1         29 die "$error\n";
157             }
158 2         8 $branch_name_pattern =~ s/\$project_prefixes/$project_prefix_regex/g;
159             }
160              
161             # Check if we are pushing any branches.
162 9         40 my @branch_names = get_pushed_branch_names( $app, $stdin );
163 9         38 $log->infof(
164             "Found %s branch(es) to push: %s.",
165             scalar( @branch_names ),
166             join( ', ', @branch_names ),
167             );
168 9 50       72 return $PLUGIN_RETURN_SKIPPED
169             if ( scalar( @branch_names ) == 0 );
170              
171             # Check if the branch names match the pattern.
172 9         15 my @incorrect_branch_names = ();
173 9         22 foreach my $branch_name ( @branch_names )
174             {
175 9 100       262 if ( $branch_name =~ $branch_name_pattern )
176             {
177 5         15 $log->infof( 'Branch %s matches the required pattern.', $branch_name );
178             }
179             else
180             {
181 4         14 $log->infof( 'Branch %s does not match the required pattern.', $branch_name );
182 4         31 push( @incorrect_branch_names, $branch_name );
183             }
184             }
185              
186 9 100       64 if ( scalar( @incorrect_branch_names ) != 0 )
187             {
188 4 50       38 my $error = sprintf(
    50          
189             "The following %s %s not match the pattern enforced by the git hooks configuration file: %s.\n" .
190             "Branches must match the following pattern: %s.",
191             scalar( @incorrect_branch_names ) == 1 ? 'branch' : 'branches',
192             scalar( @incorrect_branch_names ) == 1 ? 'does' : 'do',
193             join( ', ', @incorrect_branch_names ),
194             "/$branch_name_pattern/",
195             );
196 4         16 $log->errorf( 'Reporting error back to the hook handler! %s', $error );
197 4         67 die "$error\n";
198             }
199              
200 5         37 return $PLUGIN_RETURN_PASSED;
201             }
202              
203              
204             =head1 FUNCTIONS
205              
206             =head2 get_pushed_branch_names()
207              
208             Retrieve a list of the branches being pushed with C.
209              
210             my $tags = App::GitHooks::Plugin::ForceBranchNamePattern::get_pushed_branch_names(
211             $app,
212             $stdin,
213             );
214              
215             Arguments:
216              
217             =over 4
218              
219             =item * $app I<(mandatory)>
220              
221             An C object.
222              
223             =item * $stdin I<(mandatory)>
224              
225             The content provided by git on stdin, corresponding to a list of references
226             being pushed.
227              
228             =back
229              
230             =cut
231              
232             sub get_pushed_branch_names
233             {
234 9     9 1 22 my ( $app, $stdin ) = @_;
235 9         39 my $config = $app->get_config();
236              
237             # Analyze each reference being pushed.
238 9         43 my $branches = {};
239 9         29 foreach my $line ( @$stdin )
240             {
241 9         22 chomp( $line );
242 9         59 $log->debugf( 'Parse STDIN line >%s<.', $line );
243              
244             # Extract the branch information.
245 9         138 my ( $branch ) = ( $line =~ /^refs\/heads\/(\S+)\b/x );
246 9 50       32 next if !defined( $branch );
247 9         31 $log->infof( "Found branch '%s'.", $branch );
248 9         74 $branches->{ $branch } = 1;
249             }
250              
251 9         36 return keys %$branches;
252             }
253              
254              
255             =head1 BUGS
256              
257             Please report any bugs or feature requests through the web interface at
258             L.
259             I will be notified, and then you'll automatically be notified of progress on
260             your bug as I make changes.
261              
262              
263             =head1 SUPPORT
264              
265             You can find documentation for this module with the perldoc command.
266              
267             perldoc App::GitHooks::Plugin::ForceBranchNamePattern
268              
269              
270             You can also look for information at:
271              
272             =over
273              
274             =item * GitHub's request tracker
275              
276             L
277              
278             =item * AnnoCPAN: Annotated CPAN documentation
279              
280             L
281              
282             =item * CPAN Ratings
283              
284             L
285              
286             =item * MetaCPAN
287              
288             L
289              
290             =back
291              
292              
293             =head1 AUTHOR
294              
295             L,
296             C<< >>.
297              
298              
299             =head1 COPYRIGHT & LICENSE
300              
301             Copyright 2015-2016 Guillaume Aubert.
302              
303             This code is free software; you can redistribute it and/or modify it under the
304             same terms as Perl 5 itself.
305              
306             This program is distributed in the hope that it will be useful, but WITHOUT ANY
307             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
308             PARTICULAR PURPOSE. See the LICENSE file for more details.
309              
310             =cut
311              
312             1;