—use
warnings;
package
Git::Hooks::CheckFile;
# ABSTRACT: Git::Hooks plugin for checking files
$Git::Hooks::CheckFile::VERSION
=
'4.0.0'
;
use
v5.30.0;
use
utf8;
use
Carp;
use
Git::Hooks;
use
Path::Tiny;
my
$CFG
= __PACKAGE__ =~ s/.*::/githooks./r;
#############
# Grok hook configuration, check it and set defaults.
sub
_setup_config {
my
(
$git
) =
@_
;
my
$config
=
$git
->get_config();
$config
->{
lc
$CFG
} //= {};
my
$default
=
$config
->{
lc
$CFG
};
$default
->{sizelimit} //= [0];
$default
->{
'max-path'
} //= [0];
return
;
}
sub
check_command {
my
(
$git
,
$ctx
,
$commit
,
$file
,
$command
) =
@_
;
my
$tmpfile
=
$git
->blob(
$commit
,
$file
)
or
return
1;
# interpolate filename in $command
my
$cmd
=
$command
=~ s/\{\}/\'
$tmpfile
\'/gr;
# execute command and update $errors
my
(
$exit
,
$output
);
{
my
$tempfile
= Path::Tiny->tempfile(
UNLINK
=> 1);
## no critic (RequireBriefOpen, RequireCarping)
open
(
my
$oldout
,
'>&'
, \
*STDOUT
) or croak
"Can't dup STDOUT: $!"
;
open
(STDOUT ,
'>'
,
$tempfile
) or croak
"Can't redirect STDOUT to \$tempfile: $!"
;
open
(
my
$olderr
,
'>&'
, \
*STDERR
) or croak
"Can't dup STDERR: $!"
;
open
(STDERR ,
'>&'
, \
*STDOUT
) or croak
"Can't dup STDOUT for STDERR: $!"
;
# Let the external command know the commit that's being checked in
# case it needs to grok something from Git.
local
$ENV
{GIT_COMMIT} =
$commit
;
$exit
=
system
$cmd
;
open
(STDOUT,
'>&'
,
$oldout
) or croak
"Can't dup \$oldout: $!"
;
open
(STDERR,
'>&'
,
$olderr
) or croak
"Can't dup \$olderr: $!"
;
## use critic
$output
=
$tempfile
->slurp;
}
if
(
$exit
!= 0) {
$command
=~ s/\{\}/\'
$file
\'/g;
my
$message
=
do
{
if
(
$exit
== -1) {
"Command '$command' could not be executed: $!"
;
}
elsif
(
$exit
& 127) {
sprintf
(
"Command '%s' was killed by signal %d, %s coredump"
,
$command
, (
$exit
& 127), (
$exit
& 128) ?
'with'
:
'without'
);
}
else
{
sprintf
(
"Command '%s' failed with exit code %d"
,
$command
,
$exit
>> 8);
}
};
# Replace any instance of the $tmpfile name in the output by
# $file to avoid confounding the user.
$output
=~ s/\Q
$tmpfile
\E/
$file
/g;
$git
->fault(
$message
, {
%$ctx
,
details
=>
$output
});
return
1;
}
else
{
# FIXME: What should we do with eventual output from a
# successful command?
}
return
0;
}
sub
check_commands {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
# Construct a list of command checks from the
# githooks.checkfile.name configuration. Each check in the list is a
# pair containing a regex and a command specification.
my
@name_checks
;
foreach
my
$check
(
$git
->get_config(
$CFG
=>
'name'
)) {
my
(
$pattern
,
$command
) =
split
' '
,
$check
, 2;
if
(
$pattern
=~ m/^
qr(.)
(.*)\g{1}/) {
$pattern
=
qr/$2/
;
}
else
{
$pattern
= glob_to_regex(
$pattern
);
}
$command
.=
' {}'
unless
$command
=~ /\{\}/;
push
@name_checks
, [
$pattern
=>
$command
];
}
my
$errors
= 0;
foreach
my
$file
(
@$files
) {
my
$basename
= path(
$file
)->basename;
foreach
my
$command
(
map
{
$_
->[1]}
grep
{
$basename
=~
$_
->[0]}
@name_checks
) {
$errors
+= check_command(
$git
,
$ctx
,
$commit
,
$file
,
$command
);
}
continue
{
$git
->check_timeout();
}
}
return
$errors
;
}
sub
check_sizes {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
# See if we have to check a file size limit
my
$sizelimit
=
$git
->get_config_integer(
$CFG
=>
'sizelimit'
);
# Grok all REGEXP checks
my
%re_checks
;
foreach
(
$git
->get_config(
"$CFG.basename"
=>
'sizelimit'
)) {
my
(
$bytes
,
$regexp
) =
split
' '
,
$_
, 2;
unshift
$re_checks
{basename}{sizelimit}->@*, [
qr/$regexp/
,
$bytes
];
}
return
0
unless
$sizelimit
||
%re_checks
;
my
$errors
= 0;
foreach
my
$file
(
@$files
) {
my
$basename
= path(
$file
)->basename;
my
$size
=
$git
->file_size(
$commit
,
$file
);
my
$file_sizelimit
=
$sizelimit
;
foreach
my
$spec
(
$re_checks
{basename}{sizelimit}->@*) {
if
(
$basename
=~
$spec
->[0]) {
$file_sizelimit
=
$spec
->[1];
last
;
}
}
if
(
$file_sizelimit
&&
$file_sizelimit
<
$size
) {
$git
->fault(
<<"EOS", {%$ctx, option => '[basename.]sizelimit'});
The file '$file' is too big.
It has $size bytes but the current limit is $file_sizelimit bytes.
Please, check your configuration options.
EOS
++
$errors
;
}
}
return
$errors
;
}
sub
check_executables {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
# Grok the list of patterns to check for executable permissions
my
%executable_checks
;
foreach
my
$check
(
qw/executable not-executable/
) {
foreach
my
$pattern
(
$git
->get_config(
$CFG
=>
$check
)) {
if
(
$pattern
=~ m/^
qr(.)
(.*)\g{1}/) {
$pattern
=
qr/$2/
;
}
else
{
$pattern
= glob_to_regex(
$pattern
);
}
push
$executable_checks
{
$check
}->@*,
$pattern
;
}
}
return
0
unless
%executable_checks
;
my
$errors
= 0;
FILE:
foreach
my
$file
(
@$files
) {
my
$basename
= path(
$file
)->basename;
my
$mode
;
if
(any {
$basename
=~
$_
}
$executable_checks
{
'executable'
}->@*) {
$mode
=
$git
->file_mode(
$commit
,
$file
);
unless
(
$mode
& 0b1) {
$git
->fault(
<<"EOS", {%$ctx, option => 'executable'});
The file '$file' is not executable but should be.
Please, check your configuration options.
EOS
++
$errors
;
}
}
if
(any {
$basename
=~
$_
}
$executable_checks
{
'not-executable'
}->@*) {
if
(
defined
$mode
) {
git->fault(
<<"EOS", {%$ctx, option => '[not-]executable'});
Configuration error: The file '$file' matches a 'executable' and a
'not-executable' option simultaneously, which is inconsistent.
Please, fix your configuration so that it matches only one of these options.
EOS
++
$errors
;
}
$mode
=
$git
->file_mode(
$commit
,
$file
);
if
(
$mode
& 0b1) {
$git
->fault(
<<"EOS", {%$ctx, option => 'not-executable'});
The file '$file' is executable but should not be.
Please, check your configuration options.
EOS
++
$errors
;
}
}
}
return
$errors
;
}
sub
deny_case_conflicts {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
return
0
unless
$git
->get_config_boolean(
$CFG
=>
'deny-case-conflict'
);
# Grok the list of all files in the repository at $commit
my
@ls_files
=
split
(
/\0/,
$git
->run(
qw/ls-tree -r -z --name-only --full-tree/
,
$commit
ne
':0'
?
$commit
:
$git
->get_head_or_empty_tree),
);
my
$errors
= 0;
# Check if the new files conflict with each other
for
(
my
$i
= 0;
$i
<
$#$files
; ++
$i
) {
for
(
my
$j
=
$i
+ 1;
$j
<=
$#$files
; ++
$j
) {
if
(
lc
(
$files
->[
$i
]) eq
lc
(
$files
->[
$j
]) &&
$files
->[
$i
] ne
$files
->[
$j
]) {
++
$errors
;
$git
->fault(
<<"EOS", {%$ctx, option => 'deny-case-conflict'});
This commit adds two files with names that will conflict
with each other in the repository in case-insensitive
filesystems:
$files->[$i]
$files->[$j]
Please, rename the added files to avoid the conflict and amend your commit.
EOS
}
}
}
# Check if the new files conflict with already existing files
foreach
my
$file
(
@ls_files
) {
my
$lc_file
=
lc
$file
;
foreach
my
$name
(
@$files
) {
my
$lc_name
=
lc
$name
;
if
(
$lc_name
eq
$lc_file
&&
$name
ne
$file
) {
++
$errors
;
$git
->fault(
<<"EOS", {%$ctx, option => 'deny-case-conflict'});
This commit adds a file with a name that will conflict
with the name of another file already existing in the repository
in case-insensitive filesystems:
ADDED: $name
EXISTING: $file
Please, rename the added file to avoid the conflict and amend your commit.
EOS
}
}
}
return
$errors
;
}
sub
check_locks {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
my
@lock_checks
;
foreach
my
$pattern
(
$git
->get_config(
$CFG
=>
'lock'
)) {
if
(
$pattern
=~ m/^
qr(.)
(.*)\g{1}/) {
$pattern
=
qr/$2/
;
}
else
{
$pattern
= glob_to_regex(
$pattern
);
}
push
@lock_checks
,
qr/$pattern/
i;
}
return
0
unless
@lock_checks
;
my
@files_to_check
;
foreach
my
$file
(
@$files
) {
push
@files_to_check
,
$file
if
any {
$file
=~
$_
}
@lock_checks
;
}
return
0
unless
@files_to_check
;
# Grok the list of all lock files in the repository at $commit
my
@lock_files
=
split
(
/\0/,
$git
->run(
qw/ls-tree -z --name-only --full-tree/
,
$commit
ne
':0'
?
$commit
:
$git
->get_head_or_empty_tree,
'--'
,
map
{
"$_.lock"
}
@files_to_check
,
),
);
return
0
unless
@lock_files
;
$git
->fault(
<<"EOS", {%$ctx, option => 'lock'});
This commit adds or changes files that are locked. A file is considered locked
if there is another file with the same name and the '.lock' suffix in the same
commit. You should see who added the lock file and coordinate with them how to
integrate your changes in the changes that they may have already be working
on. Wait for them to perform a commit and to remove the lock file. Then, you
should create a lock of your own, manually integrate your changes into theirs,
and commit your changes while removing your lock.
These are the locked files that this commit tried to add or change:
@{[join("\n ", map {s/\.lock$//r} @lock_files)]}
EOS
return
scalar
@lock_files
;
}
sub
check_max_paths {
my
(
$git
,
$ctx
,
$commit
,
$files
) =
@_
;
# See if we have to check a file size limit
my
$max_path
=
$git
->get_config_integer(
$CFG
=>
'max-path'
);
return
0
unless
$max_path
;
my
@bigs
=
grep
{
length
>
$max_path
}
@$files
;
if
(
@bigs
) {
$git
->fault(
<<"EOS", {%$ctx, option => 'max-path'});
The following files have paths more than $max_path characters long.
@{[join("\n ", @bigs)]}
Git may not be able to check them out on a Windows host due to its maximum path
length limit of 260 characteres. See:
Please, ammend your changes to shorten their paths.
EOS
}
return
@bigs
;
}
# Assign meaningful names to action codes.
my
%ACTION
= (
A
=>
'add'
,
M
=>
'modify'
,
D
=>
'delete'
,
);
sub
check_acls {
my
(
$git
,
$ctx
,
$name2status
) =
@_
;
my
@acls
=
eval
{
$git
->grok_acls(
$CFG
,
'AMD'
) };
if
($@) {
$git
->fault($@,
$ctx
);
return
1;
}
return
0
unless
@acls
;
# Collect the ACL errors and group them by ACL/ACTION so that we can produce
# more compact error messages.
my
%acl_errors
;
FILE:
foreach
my
$file
(
sort
keys
%$name2status
) {
my
$statuses
=
$name2status
->{
$file
};
foreach
my
$acl
(
@acls
) {
next
unless
ref
$acl
->{spec} ?
$file
=~
$acl
->{spec} :
$file
eq
$acl
->{spec};
# $status is usually a single letter but it can be a string of
# letters if we grokked affected files in a merge commit. So, we
# consider a match if the intersection of the two strings ($statuses
# and $acl->{action}) is not empty.
next
if
none {
index
(
$acl
->{action},
$_
) >= 0}
split
//,
$statuses
;
unless
(
$acl
->{allow}) {
my
$action
=
$ACTION
{
$statuses
} ||
$statuses
;
push
$acl_errors
{
$acl
->{acl}}{
$action
}->@*,
$file
;
}
next
FILE;
}
}
if
(
%acl_errors
) {
my
$myself
=
$git
->authenticated_user();
my
%context
= (
%$ctx
,
option
=>
'acl'
);
while
(
my
(
$acl
,
$actions
) =
each
%acl_errors
) {
while
(
my
(
$action
,
$files
) =
each
%$actions
) {
my
$these_files
=
scalar
(
@$files
) > 1 ?
'these files'
:
'this file'
;
$git
->fault(
<<"EOS", \%context);
Authorization error: you ($myself) cannot $action $these_files:
@{[join("\n ", @$files)]}
Due to the following acl:
$acl
EOS
}
}
}
return
scalar
%acl_errors
;
}
sub
check_everything {
my
(
$git
,
$ref
,
$commit
,
$extra
) =
@_
;
# The $extra information was generated by the --name-status --cc options to
# git-log. It has one line for each file affected in the commit. Merge
# commits only show files with conflicts. The format is "<S>+\t<FILE>". <S>
# is one letter indicating how the file was affected, as documented in the
# --diff-filter option. <FILE> is the file path since the repository root,
# without a leading slash.
my
%name2status
;
if
(
defined
$extra
) {
foreach
(
split
/\n/,
$extra
) {
if
(/^(?<status>[ACDMRTUXB0-9]+)\t(?<file>.+)/) {
my
(
$status
,
$file
) = ($+{status}, $+{file});
if
(
$file
=~ /^\".*\"$/) {
# Pathnames with "unusual" characters are quoted as
# explained for the configuration variable core.quotePath
# (see git-config(1)): "by enclosing the pathname in
# double-quotes and escaping those characters with
# backslashes in the same way C escapes control characters
# (e.g. \t for TAB, \n for LF, \\ for backslash) or bytes
# with values larger than 0x80 (e.g. octal \302\265 for
# "micro" in UTF-8)."
# The section "Quote and Quote-like Operators" of perlop
# explains how Perl's string literal syntax is an (almost)
# superset of C's. The only C escape that Perl doesn't have
# is "\v", which can be expressed in Perl as "\x0b". So,
# first we use a s/// operator to replace any and all
# occurrences of \v.
$file
=~ s/(?<!\\)\\v/\x0b/g;
# However, since we must evaluate the literal string as
# doubly-quoted we must take care to escape the $ and @
# sigils, or Perl will try to innterpolate them.
$file
=~ s/([\$\@])/\\$1/g;
# Now, we can use eval to read the resulting string as if it
# were a Perl string literal.
$file
=
eval
$file
;
## no critic (ProhibitStringyEval)
}
$name2status
{
$file
} =
$status
;
}
}
}
my
%context
= (
ref
=>
$ref
);
$context
{commit} =
$commit
unless
$commit
eq
':0'
;
my
$errors
= check_acls(
$git
, \
%context
, \
%name2status
);
if
(
my
@AC_files
=
sort
grep
{
$name2status
{
$_
} =~ /[AC]/}
keys
%name2status
) {
$errors
+=
deny_case_conflicts(
$git
, \
%context
,
$commit
, \
@AC_files
) +
check_max_paths(
$git
, \
%context
,
$commit
, \
@AC_files
);
}
if
(
my
@ACM_files
=
sort
grep
{
$name2status
{
$_
} =~ /[ACM]/}
keys
%name2status
) {
$errors
+=
check_executables(
$git
, \
%context
,
$commit
, \
@ACM_files
) +
check_locks(
$git
, \
%context
,
$commit
, \
@ACM_files
) +
check_sizes(
$git
, \
%context
,
$commit
, \
@ACM_files
);
# Avoid external checks if there are errors already
$errors
+= check_commands(
$git
, \
%context
,
$commit
, \
@ACM_files
)
unless
$errors
;
}
return
$errors
;
}
sub
check_ref {
my
(
$git
,
$ref
) =
@_
;
my
(
$old_commit
,
$new_commit
) =
$git
->get_affected_ref_range(
$ref
);
my
@commits
=
$git
->get_commits(
$old_commit
,
$new_commit
,
[
qw/--name-status --ignore-submodules -r --cc/
],
);
my
$errors
= 0;
foreach
my
$commit
(
@commits
) {
$errors
+= check_everything(
$git
,
$ref
,
$commit
->commit,
$commit
->extra);
}
return
$errors
;
}
sub
check_commit {
my
(
$git
,
$current_branch
) =
@_
;
my
$extra
=
$git
->run(
qw/diff-index --name-status --ignore-submodules --no-commit-id --cached -r/
,
$git
->get_head_or_empty_tree);
return
check_everything(
$git
,
$current_branch
,
':0'
,
# mark to signify the index
$extra
,
);
}
sub
check_patchset {
my
(
$git
,
$branch
,
$commit
) =
@_
;
return
check_everything(
$git
,
$branch
,
$commit
->commit,
$commit
->extra);
}
# Install hooks
my
$options
= {
config
=> \
&_setup_config
};
GITHOOKS_CHECK_AFFECTED_REFS(\
&check_ref
,
$options
);
GITHOOKS_CHECK_PRE_COMMIT(\
&check_commit
,
$options
);
GITHOOKS_CHECK_PATCHSET(\
&check_patchset
,
$options
);
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Git::Hooks::CheckFile - Git::Hooks plugin for checking files
=head1 VERSION
version 4.0.0
=head1 SYNOPSIS
As a C<Git::Hooks> plugin you don't use this Perl module directly. Instead, you
may configure it in a Git configuration file like this:
[githooks]
# Enable the plugin
plugin = CheckFile
# These users are exempt from all checks
admin = joe molly
# These groups are used in ACL specs below
groups = architects = tiago juliana
groups = dbas = joao maria
[githooks "checkfile"]
# Check specific files with specific commands
name = *.p[lm] perlcritic --stern --verbose 10
name = *.pp puppet parser validate --verbose --debug
name = *.pp puppet-lint --no-variable_scope-check --no-documentation-check
name = *.sh bash -n
name = *.sh shellcheck --exclude=SC2046,SC2053,SC2086
name = *.yml yamllint
name = *.js eslint -c ~/.eslintrc.json
# Reject files bigger than 1MiB
sizelimit = 1M
# Reject files with names that would conflict with other files in the
# repository in case-insensitive filesystems, such as the ones on Windows.
deny-case-conflict = true
# Subject MS-Office and OpenDocument documents to the locking mechanism
lock = *.docx
lock = *.pptx
lock = *.xlsx
lock = qr/\\.(od[fgpst])$/
# Reject commits adding scripts without the executable bit set.
executable = *.sh
executable = *.csh
executable = *.ksh
executable = *.zsh
# Reject commits adding source files with the executable bit set.
not-executable = qr/\\.(?:c|cc|java|pl|pm|txt)$/
# Only architects may add, modify, or delete pom.xml files.
acl = deny AMD ^(?i).*pom\\.xml
acl = allow AMD ^(?i).*pom\\.xml by @architects
# Only dbas may add or delete SQL files under database/
acl = deny AD ^database/.*\\.sql$
acl = allow AD ^database/.*\\.sql$ by @dba
# Reject new files containing dangerous characters, avoiding names which may
# cause problems.
acl = deny A ^.*[^a-zA-Z0-1/_.-]
=head1 DESCRIPTION
This L<Git::Hooks> plugin hooks itself to the hooks below to check if the
names and contents of files added to or modified in the repository meet
specified constraints. If they don't, the commit/push is aborted.
=over
=item * B<pre-applypatch>
=item * B<pre-commit>
=item * B<update>
=item * B<pre-receive>
=item * B<ref-update>
=item * B<patchset-created>
=item * B<draft-published>
=back
To enable it you should add it to the githooks.plugin configuration
option:
[githooks]
plugin = CheckFile
=for Pod::Coverage check_command check_commands check_sizes check_executables check_max_paths deny_case_conflicts check_locks check_acls check_everything check_ref check_commit check_patchset
=head1 NAME
CheckFile - Git::Hooks plugin for checking files
=head1 CONFIGURATION
The plugin is configured by the following git options under the
C<githooks.checkfile> subsection.
It can be disabled for specific references via the C<githooks.ref> and
C<githooks.noref> options about which you can read in the L<Git::Hooks>
documentation.
=head2 name PATTERN COMMAND
This directive tells which COMMAND should be used to check files matching
PATTERN.
Only the file's basename is matched against PATTERN.
PATTERN is usually expressed with
L<globbing|https://metacpan.org/pod/File::Glob> to match files based on
their extensions, for example:
[githooks "checkfile"]
name = *.pl perlcritic --stern
If you need more power than globs can provide you can match using L<regular
expressions|http://perldoc.perl.org/perlre.html>, using the C<qr//>
operator, for example:
[githooks "checkfile"]
name = qr/xpto-\\d+.pl/ perlcritic --stern
COMMAND is everything that comes after the PATTERN. It is invoked once for
each file matching PATTERN with the name of a temporary file containing the
contents of the matching file passed to it as a last argument. If the
command exits with any code different than 0 it is considered a violation
and the hook complains, rejecting the commit or the push.
If the filename can't be the last argument to COMMAND you must tell where in
the command-line it should go using the placeholder C<{}> (like the argument
to the C<find> command's C<-exec> option). For example:
[githooks "checkfile"]
name = *.xpto cmd1 --file {} | cmd2
COMMAND is invoked as a single string passed to C<system>, which means it
can use shell operators such as pipes and redirections.
Some real examples:
[githooks "checkfile"]
name = *.p[lm] perlcritic --stern --verbose 5
name = *.pp puppet parser validate --verbose --debug
name = *.pp puppet-lint --no-variable_scope-check
name = *.sh bash -n
name = *.sh shellcheck --exclude=SC2046,SC2053,SC2086
name = *.erb erb -P -x -T - {} | ruby -c
COMMAND may rely on the B<GIT_COMMIT> environment variable to identify the
commit being checked according to the hook being used, as follows.
Since the external commands may take much time to run, the plugin checks if the
C<githooks.timeout> option has been violated after each command runs.
=over
=item * B<pre-commit>
This hook does not check a complete commit, but the index tree. So, in this
case the variable is set to F<:0>. (See C<git help revisions>.)
=item * B<update, pre-receive, ref-updated>
In these hooks the variable is set to the SHA1 of the new commit to which
the reference has been updated.
=item * B<patchset-created, draft-published>
In these hooks the variable is set to the argument of the F<--commit> option
(a SHA1) passed to them by Gerrit.
=back
The reason that led to the introduction of the GIT_COMMIT variable was to
enable one to invoke an external command to check files which needed to grok
some configuration from another file in the repository. Specifically, we
wanted to check Python scripts with the C<pylint> command passing to its
C<--rcfile> option the configuration file F<pylint.rc> sitting on the
repository root. So, we configured CheckFile like this:
[githooks "checkfile"]
name = *.py mypylint.sh
And the F<mypylint.sh> script was something like this:
#!/bin/bash
# Create a temporary file do save the pylint.rc
RC=$(tempfile)
trap 'rm $RC' EXIT
git cat-file $GIT_COMMIT:pylint.rc >$RC
pylint --rcfile=$RC "$@"
=head2 sizelimit INT
This directive specifies a size limit (in bytes) for any file in the
repository. If set explicitly to 0 (zero), no limit is imposed, which is the
default. But it can be useful to override a global specification in a particular
repository.
=head2 basename.sizelimit BYTES REGEXP
This directive takes precedence over the C<githooks.checkfile.sizelimit> for
files which basename matches REGEXP.
=head2 deny-case-conflict BOOL
This directive checks for newly added files that would conflict in
case-insensitive file-systems.
Git itself is case-sensitive with regards to file names. Many operating system's
file-systems are case-sensitive too, such as Linux, macOS, and other Unix-derived
systems. But Windows's file-systems are notoriously case-insensitive. So, if you
want your repository to be absolutely safe for Windows users you don't want to
add two files which filenames differ only in a case-sensitive manner. Enable
this option to be safe
Note that this check have to check the newly added files against all files
already in the repository. It can be a little slow for large repositories. Take
heed!
=head2 lock PATTERN
This multi-valued directive implements a poor man's version of the L<Subversion
locking
PATTERNs specified by one or more of these directives match, case-insensitively,
the files that should be subject to the locking checks. Whenever one pushes a
commit which creates or copies a file with a name matching one of the PATTERNs,
the hook checks if the file is locked. If it is, the commit is rejected.
The PATTERN argument can be expressed either as a globbing pattern or as a
regular expression. Take a look at the explanation for the PATTERN argument to
the C<name> directive above for the details.
To lock a file you just need to create another file with the same name and
suffixed with F<.lock>. For instance, suppose you're going to edit a file called
F<doc/file.odt>. In order to signal to your colleagues that they should refrain
from editing the file, just create another file called F<doc/file.odt.lock> and
commit it. The lock file may even be empty.
If a colleague doesn't notice the lock and tries to push a new version of the
file, the push is rejected telling them that the file is locked and that they
should coordinate with you how to integrate their changes into yours.
When you commit the changes to the file, you should delete the lock file in the
same commit. This way, when you push the commit, the commit will be accepted,
the file will be changed and the lock removed.
Note that this mechanism isn't able to alert users that they shouldn't edit
locked files. In Subversion, files subject to locking are kept read-only so that
the users have to execute the C<svn lock> command before editing them. There is
no such alert in Git. The users should really remember to create a lock file
before editing any file. If they forget, they will be reminded that the file is
locked when they try to push them, which will avoid the race condition, but it
will be too late to avoid the need to manually integrate the changes later. You
should consider this mechanism as an informational protocol only. All people
involved in changing the repository must learn to pay attention to it.
=head2 max-path LENGTH
This directive checks for newly added or copied files if their full path is at
most LENGTH characters long.
This is useful to avoid problems with the L<Windows maximum path length
which is defined as 260 characters.
Note that this check counts just the path length inside the repository. When
cloning it on Windows you have to make some allowance for the path length of the
clone's root directory. So, you should allow less than 260 characters!
=head2 executable PATTERN
This directive requires that all added or modified files with names matching
PATTERN must have the executable permission. This allows you to detect common
errors such as forgetting to set scripts as executable.
PATTERN is specified as described in the C<githooks.checkfile.name> directive
above.
You can specify this option multiple times so that all PATTERNs are considered.
=head2 non-executable PATTERN
This directive requires that all added or modified files with names matching
PATTERN must B<not> have the executable permission. This allows you to detect
common errors such as setting source code as executable.
PATTERN is specified as described in the C<githooks.checkfile.name> directive
above.
You can specify this option multiple times so that all PATTERNs are considered.
=head2 acl RULE
This multi-valued option specifies rules allowing or denying specific users to
perform specific actions on specific files. By default any user can perform any
action on any file. So, the rules are used to impose restrictions.
The acls are grokked by the L<Git::Repository::Plugin::GitHooks>'s C<grok_acls>
method. Please read its documentation for the general documentation.
A RULE takes three or four parts, like this:
(allow|deny) [AMD]+ <filespec> (by <userspec>)?
Some parts are described below:
=over 4
=item * B<[AMD]+>
The second part specifies which actions are being considered by a combination of
letters: (A) for files added, (M) for files modified, and (D) for files
deleted. (These are the same letters used in the C<--diff-filter> option of the
C<git diff-tree> command.) You can specify one, two, or the three letters.
=item * B<< <filespec> >>
The third part specifies which files are being considered. In its simplest form,
a C<filespec> is a complete path beginning at the repository root, without a
leading slash (e.g. F<lib/Git/Hooks.pm>). These filespecs match a single file
exactly.
If the C<filespec> starts with a caret (^) it's interpreted as a Perl regular
expression, the caret being kept as part of the regexp. These filespecs match
potentially many files (e.g. F<^lib/.*\\.pm$>).
=back
See the L</SYNOPSIS> section for some examples.
=head1 AUTHOR
Gustavo L. de M. Chaves <gnustavo@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2024 by CPQD <www.cpqd.com.br>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut