Using Precommit Hooks For Static Code Analysis
In our last several articles, we’ve discussed how to use the PHP_CodeSniffer library to verify our code is following an agreed-upon coding standard and how to run the phpcs extension in VSCode to see where we’re not following the code as we type. We’ll eventually discuss how to move these checks to a Continuous Integration server but wouldn’t it be great if we could make sure our code was passing these checks before we push them anywhere?
In this article we’ll discuss how to use Git’s pre-commit hooks to run our static code analysis tools on just the set of files we’ve modified.
Exploring the .git Directory
When we run git init
on a directory the command creates a “.git” directory inside of our current directory that git uses to maintain all of its data associated with our repository. We’re not going to go into most of the information here but some of the more important pieces are:
- The
config
file containing settings about our repository. - The
objects
directory contains all of the diffs for our repository - The
hooks
directory contains the hook scripts.
% ls -l .git
total 104
-rw-r--r-- 1 scottkeck-warren staff 3798 Apr 27 20:25 COMMIT_EDITMSG
-rw-r--r-- 1 scottkeck-warren staff 100 Mar 4 19:44 FETCH_HEAD
-rw-r--r-- 1 scottkeck-warren staff 23 Mar 4 19:44 HEAD
-rw-r--r-- 1 scottkeck-warren staff 41 Mar 4 19:44 ORIG_HEAD
-rw-r--r-- 1 scottkeck-warren staff 510 Mar 3 20:07 config
-rw-r--r-- 1 scottkeck-warren staff 73 Apr 27 2020 description
drwxr-xr-x 13 scottkeck-warren staff 416 Apr 27 2020 hooks
-rw-r--r-- 1 scottkeck-warren staff 25072 Apr 27 20:25 index
drwxr-xr-x 3 scottkeck-warren staff 96 Apr 27 2020 info
drwxr-xr-x 4 scottkeck-warren staff 128 Apr 27 2020 logs
drwxr-xr-x 253 scottkeck-warren staff 8096 Apr 27 20:23 objects
drwxr-xr-x 5 scottkeck-warren staff 160 May 17 2020 refs
Git Hooks
The focus of this article is to discuss how to use the hooks directory to run our static code analysis tools. We can place scripts in this directory that will get run before certain actions occur in our repository. Like before we push (pre-push
), after we update (post-update
), and important for our discussion before we commit (pre-commit
).
The hooks directory comes with sample scripts so we know what we can do.
% ls -l .git/hooks
total 96
-rwxr-xr-x 1 scottkeck-warren staff 478 Apr 27 2020 applypatch-msg.sample
-rwxr-xr-x 1 scottkeck-warren staff 896 Apr 27 2020 commit-msg.sample
-rwxr-xr-x 1 scottkeck-warren staff 3327 Apr 27 2020 fsmonitor-watchman.sample
-rwxr-xr-x 1 scottkeck-warren staff 189 Apr 27 2020 post-update.sample
-rwxr-xr-x 1 scottkeck-warren staff 424 Apr 27 2020 pre-applypatch.sample
-rwxr-xr-x 1 scottkeck-warren staff 1638 Apr 27 2020 pre-commit.sample
-rwxr-xr-x 1 scottkeck-warren staff 1348 Apr 27 2020 pre-push.sample
-rwxr-xr-x 1 scottkeck-warren staff 4898 Apr 27 2020 pre-rebase.sample
-rwxr-xr-x 1 scottkeck-warren staff 544 Apr 27 2020 pre-receive.sample
-rwxr-xr-x 1 scottkeck-warren staff 1492 Apr 27 2020 prepare-commit-msg.sample
-rwxr-xr-x 1 scottkeck-warren staff 3610 Apr 27 2020 update.sample
One of the more annoying parts about the Git Hooks is that while they’re in our git repository they’re not kept as part of the repository. That means that when we add the file to our repository it won’t automatically show up in the rest of the teams .git/hooks directory.
Our solution to get around that is to keep a copy of the pre-commit script outside the .git directory and then recommend people install it into their hooks directory. It’s not ideal but it does work. There are also third-party scripts that will help us maintain them (and we may discuss that in another article).
Our First Pre-Commit Script
To get started with our pre-commit script we’re going to make it as simple as possible and just run phpcs on both the app
and tests
directory of our sample Laravel project.
#!/usr/bin/env bash
./vendor/bin/phpcs --standard=PSR12 app tests
Now before we’re allowed to commit our code (pre-commit) our script will be run and we won’t be allowed to create the commit if our code is found in violation of the standards.
For example:
% touch test.md
% git add test.md
% git commit -m "test"
FILE: /var/www/tests/CreatesApplication.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 1 LINE
----------------------------------------------------------------------
16 | ERROR | [x] Expected at least 1 space before "."; 0 found
16 | ERROR | [x] Expected at least 1 space after "."; 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
Time: 247ms; Memory: 10MB
The amazing thing about this is because the script is part of the git repository most tools we use to interact with the repository will also run this script before creating the commit. For example, VSCode will display an error and ask us to refer to the log to figure out what to do to fix it.
** image here **
Now there are two downsides to the very basic script we created. The first is that the file we were getting errors on wasn’t even a file we were trying to commit. We didn’t cause the problem but it’s preventing us from committing our changes and requires us to spend time fixing someone else’s code which will make our pull request harder to review. The second is that even though we’re only making changes to one file it’s running the checks on all of our files. As we add tools to our pre-commit script and files to our project the amount of time it’s going to take to run all of those files will get unbearably long.
Our Second Pre-Commit Script
To help us with the problems we outlined above we’re going to turn to git-diff
to help us find just the files that we’ve “staged” for the commit so we can run static analysis against them. To do this we’re going to use the following command.
git diff --diff-filter=AM --name-only --cached app tests | grep ".php$"
Let’s break this down so we can troubleshoot this.
git diff
-> Runs the git diff command which shows us changes in our repository--diff-filter=AM
-> filters out files to only show us modifications and additions--name-only
-> returns just the name of the file and not the contents--cached
-> returns changes that have been staged for the next commit and not every changed fileapp tests
-> limit our results to files in the app and tests directories| grep ".php$"
-> Limit our results to just.php
files
Now this list could be extremely lengthy and some tools only accept a single file at a time so instead of just pushing all the files to our static code analysis tools we’re going to pipe them through xargs
so each one gets executed individually. Finally, we’re going to create a helper function to enable us to more easily add more static code analysis tools later.
#!/usr/bin/env bash
function __runStaticTool() #(name, command)
{
echo -e "\n\n$1"
output=$(eval "$2" 2>&1)
exitcode=$?
if [[ 0 == $exitcode || 130 == $exitcode ]]; then
echo -e "Success"
else
echo -e "Failure\n\n$output"
exit 1
fi
}
modified="git diff --diff-filter=AM --name-only --cached app tests | grep \".php$\""
ignore="resources/lang,resources/views,bootstrap/helpers,database/migrations,bin"
__runStaticTool "PHP_CodeSniffer" "${modified} | xargs vendor/bin/phpcs --standard=PSR12 --ignore=${ignore}"
Now if we attempt to commit our code we’ll get much nicer results.
% git commit -m "test"
PHP_CodeSniffer
Success
[master 420753f] test
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100755 test2.md
In future articles, we’ll add more tools to this script to make it even more powerful.
What You Need To Know
- Git allows us to define scripts to perform validation checks
- pre-commit checks if a commit is valid
git diff
allows us to find just the changed files we need
Scott Keck-Warren
Scott is the Director of Technology at WeCare Connect where he strives to provide solutions for his customers needs. He's the father of two and can be found most weekends working on projects around the house with his loving partner.
Top Posts
- Working With Soft Deletes in Laravel (By Example)
- Fixing CMake was unable to find a build program corresponding to "Unix Makefiles"
- Upgrading to Laravel 8.x
- Get The Count of the Number of Users in an AD Group
- Multiple Vagrant VMs in One Vagrantfile
- Fixing the "this is larger than GitHub's recommended maximum file size of 50.00 MB" error
- Changing the Directory Vagrant Stores the VMs In
- Accepting Android SDK Licenses From The OSX Command Line
- Fixing the 'Target class [config] does not exist' Error
- Using Rectangle to Manage MacOS Windows