GIT Pre-Commit Hooks
GIT pre-* hooks allow for some very useful checks before committing or pushing changes. Of course there are more hooks than just the pre-commit or pre-push hook but I found these two the most helpful as they allow to implement checks before making changes public. Of course many build processes still perform a lot of checks before the merge process on the remote server but as a user I find it very helpful to know in advance if my changes will be accepted or not. Additionally pre-* hooks help with enforcing a uniform code style and code quality.
The following implementations are for bash (linux) only.
Create Pre-Commit Hooks
Pre-* hooks can be created in the .git/hooks
directory of your project. For a pre-commit hook just create the file pre-commit
in this directory and you are good to go with creating your pre-commit checks in here. You may want to consider to create your pre-commit checks in separate files and just link to them in your pre-commit file. This way you can re-use your pre-commit hooks in other projects and structure them in a more easy way.
What I recommend to to is simply link to a delegator script in your project for all of your pre-commit checks
Sample Pre-Commit Hook
#!/bin/bash
strindex() {
x="${1%%$2*}"
[[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
}
orgpath="$(pwd)"
repository="Your repository name here" # Put the name of your repository in here
pos=$(strindex "$orgpath" "$repository")
length=$pos+${#repository}
rootpath=${orgpath:0:length} # The root path is required for absolute path reference in the following scripts
. ${rootpath}/Build/Hooks/delegator.sh # adjust the path to your liking
In the delegator scripts you can then call all the different checks you want to perform. Example of the delegator script:
#!/bin/bash
# Include scripts containing bash functions for checking your code.
. ${rootpath}/Build/Hooks/filename.sh
. ${rootpath}/Build/Hooks/logging.sh
. ${rootpath}/Build/Hooks/syntax.sh
. ${rootpath}/Build/Hooks/tests.sh
for FILE in $(git diff --cached --name-only); do
# Code checks are performed here and on failure should exit with none-zero codes which abort the commit
done
I like to structure my code checks in functions and group them by different purposes which is why I put them in different scripts. Instead of checking all files on every commit I usually only check the changed files which is done with this line:
for FILE in $(git diff --cached --name-only); do
The advantage of this is that the pre-commit hook is very fast and doesn't slow down the commit process. For this purpose I also don't perform my unit tests on commits. Commits should be done often and a long test of all unit tests discourages developers from committing. From my perspective it makes more sense to perform the unit tests before the push
is executed and since I checked all the other things on every commit my pre-push
hook simply contains a single line which runs my unit tests.
Sample Pre-Commit Checks
filename.sh
The filename check confirms that a file only contains ASCII characters.
#!/bin/bash
isValidFileName() {
if test $(git diff --cached --name-only --diff-filter=A -z "$1" |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
echo 0
return 0
fi
echo 1
return 1
}
The check in the delegator looks like this:
if [[ $(isValidFileName "$FILE") = 0 ]]; then
echo -e "\e[1;31m\tInvalid file name '$FILE'.\e[0m" >&2
exit 1
fi
logging.sh
During development you sometimes log variables and values which are not meant for production. In order to find these accidental logs these two functions can be helpful.
#!/bin/bash
hasPhpLogging() {
RESULT=$(grep "var_dump(" "$1")
if [ ! -z $RESULT ]; then
echo 1
return 1
fi
echo 0
return 0
}
hasJsLogging() {
RESULT=$(grep "console.log(" "$1")
if [ ! -z $RESULT ]; then
echo 1
return 1
fi
echo 0
return 0
}
The check in the delegator looks like this:
if [[ "$FILE" =~ ^.+(php)$ ]] && [[ $(hasPhpLogging "$FILE") = 1 ]]; then
echo -e "\e[1;33m\tWarning, the commit contains a call to var_dump() in '$FILE'. Commit was not aborted, however.\e[0m" >&2
fi
if [[ "$FILE" =~ ^.+(js)$ ]] && [[ $(hasJsLogging "$FILE") = 1 ]]; then
echo -e "\e[1;33m\tWarning, the commit contains a call to console.log()
fi
These checks are only performed on .php
and .js
files respectively. Please note that these checks will not abort the commit (no exit 1
is specified) since I prefer them to only be warnings instead of errors.
syntax.sh
For php I perform three checks: linting, phpcs and phpmd. For phpcs and phpmd you of course have to install them and create the corresponding configuration files.
#!/bin/bash
hasInvalidPhpSyntax() {
# php lint
$(php -l "$1" > /dev/null)
if [[ $? != 0 ]]; then
echo 1
return 1
fi
# phpcs
$(php -d memory_limit=4G ${rootpath}/vendor/bin/phpcs --standard="${rootpath}/Build/Config/phpcs.xml" --encoding=utf-8 -n -p "$1" > /dev/null)
if [[ $? != 0 ]]; then
echo 2
return 2
fi
# phpmd
$(php -d memory_limit=4G ${rootpath}/vendor/bin/phpmd "$1" text ${rootpath}/Build/Config/phpmd.xml --exclude *tests* --minimumpriority 1 > /dev/null)
if [[ $? != 0 ]]; then
echo 3
return 3
fi
echo 0
return 0
}
In the delegator the following code has to be implemented:
if [[ "$FILE" =~ ^.+(php)$ ]]; then
PHP_SYNTAX=$(hasInvalidPhpSyntax "$FILE")
if [[ $PHP_SYNTAX = 1 ]]; then
echo -e "\e[1;31m\tPhp linting error.\e[0m" >&2
exit 1
fi
if [[ $PHP_SYNTAX = 2 ]]; then
echo -e "\e[1;31m\tCode Sniffer error.\e[0m" >&2
exit 1
fi
if [[ $PHP_SYNTAX = 3 ]]; then
echo -e "\e[1;31m\tMess Detector error.\e[0m" >&2
exit 1
fi
fi
For my bash scripts I use bash to validate them.
isValidBashScript() {
bash -n "$1" 1> /dev/null
if [ $? -ne 0 ]; then
echo 0
return 0
fi
echo 1
return 1
}
In the delegator the following code has to be implemented:
if [[ "$FILE" =~ ^.+(sh)$ ]] && [[ $(isValidBashScript "$FILE") = 0 ]]; then
echo -e "\e[1;31m\tBash linting error in '$FILE'.\e[0m" >&2
exit 1
fi
Some additional general checks I perform are that a line doesn't end with a whitespace and no tabs are used.
hasInvalidBasicSyntax() {
# Check whitespace end of line in code
if [[ -n $(grep -P ' $' "$1") ]]; then
echo 1
return 1
fi
# Check for tabs
if [[ -n $(grep -P '\t' "$1") ]]; then
echo 2
return 2
fi
echo 0
return 0
}
In the delegator the following code has to be implemented:
if [[ "$FILE" =~ ^.+(sh|js|php|json|css)$ ]]; then
GEN_SYNTAX=$(hasInvalidBasicSyntax "$FILE")
if [[ $GEN_SYNTAX = 1 ]]; then
echo -e "\e[1;31m\tFound whitespace at end of line in $FILE.\e[0m" >&2
grep -P ' $' $FILE >&2
exit 1
fi
if [[ $GEN_SYNTAX = 2 ]]; then
echo -e "\e[1;31m\tFound tab instead of whitespace $FILE.\e[0m" >&2
grep -P '\t' $FILE >&2
exit 1
fi
fi
tests.sh
As described above unit tests may take too long to run for a simple commit. Static analysis on the other hand on single files can be performed very quick which allows us to run at least them on a commit basis.
#!/bin/bash
isPhanTestSuccessful() {
php -d memory_limit=4G ${rootpath}/vendor/bin/phan -k ${rootpath}/Build/Config/phan.php -f "$1"
if [ $? -ne 0 ]; then
echo 0
return 0
fi
echo 1
return 1
}
isPhpStanTestSuccessful() {
php -d memory_limit=4G ${rootpath}/vendor/bin/phpstan analyse --autoload-file=${rootpath}/phpOMS/Autoloader.php -l 7 -c ${rootpath}/Build/Config/phpstan.neon "$1"
if [ $? -ne 0 ]; then
echo 0
return 0
fi
echo 1
return 1
}
In the delegator the following code has to be implemented:
if [[ "$FILE" =~ ^.+(php)$ ]] && [[ $(isPhanTestSuccessful "$FILE") = 0 ]]; then
echo -e "\e[1;31m\tPhan error.\e[0m" >&2
exit 1
fi
if [[ "$FILE" =~ ^.+(php)$ ]] && [[ $(isPhpStanTestSuccessful "$FILE") = 0 ]]; then
echo -e "\e[1;31m\tPhp stan error.\e[0m" >&2
exit 1
fi
Additional Pre-Commit Checks
Additional pre-commit checks could be:
- Spell checking for commit messages
- Check if html tags are implemented correctly (e.g. images must have a alt attribute, inline styles are not allowed etc..)
- JavaScript linting
- CSS linting
- Project specific checks (e.g. no hard coded localization, only allow SVG and PNG images, every model needs to have a unit test, ...)
Some Snippets For Html
The following snippets can be used to check correct html tag implementation.
Alt attribute for images is required:
if [[ -n $(grep -P '(\<img)((?!.*?alt=).)*(>)' "$1") ]]; then
echo 1
return 1
fi
Input elements must have a type attribute:
if [[ -n $(grep -P '(<input)((?!.*?type=).)*(>)' "$1") ]]; then
echo 1
return 1
fi
Inline CSS is invalid:
if [[ -n $(grep -P '(style=)' "$1") ]]; then
echo 1
return 1
fi