Hacking It Out: Enforcing Code Quality with Hooks and Scripts

Image for postDon’t be like her. Seriously.

The Problem

In order to maintain some form of sanity in your codebase, you’ve set up some basic code style and quality checks (linting) using tools such as JSHint and ESLint (for JavaScript) and PHP_CodeSniffer (for PHP). So, typically, before a dev pushes their code, they’d run jscs ., eshint ., or phpcs -h . to run the linting scripts and be sure there are no errors. The problem, though, is that they forget to do that sometimes, and bad things can happen when the code gets to the remote.

The Solution

Git Hooks to the rescue! Git hooks provide a means of executing custom scripts at specific points in your Git workflow. For instance, you could:

  • ensure that commit messages conform to a particular standard (such as minimum length and format)
  • send notifications to specific people whenever there’s a new commit, push, or merge
  • ensure teammates always checkout from specific branches
  • limit the number of changed files per commit (useful in case you prefer small commits)
  • modify all commit messages so they include, for instance, the user’s location (for whatever reason you want it)
  • update API documentation
  • run tests
  • automatically run code quality checks whenever there’s a new commit, push or merge (like we’re going to be doing)

There are lots of git hooks available, but the one we need to solve this is the pre-commit hook. The pre-commit hook is quite easy to figure out: it’s essentially a shell script that runs whenever someone tries to commit. Within that script, you can execute whatever you wish. All you need to do to stop that commit from going through is exit with a status code that’s not zero.

Let’s write a very simple pre-commit hook. Don’t worry if you’re not familiar with Bash, most of the syntax we need is quite simple. Here’s what you need to know:

  • Most of the linting and test scripts exit with a non-zero status code if there are any errors, so we can simply check the exit code of the last command (using $?) to know if there were any errors
  • We’ll add up all the error codes from all the operations and stop the commit if the sum is greater than 0
#!/bin/bash
# We'll only runchecks on changes that are a part of this commit
# So let's stash others
git stash -q --keep-index

## Now we can do our stuff...
# First, we check code quality
echo 'Checking Javascript code quality...'
jscs ./js
# EC here is just a shortcut for "Exit Code"
JSCS_EC=$?
eshint .
ESHINT_EC=$?
echo 'Checking PHP code style...'
phpcs -h . --standard=PSR2
PHPCS_EC=$?

# Then, we run tests
echo 'Running tests...'
phpunit
PHPUNIT_EC=$?

# We're done with checks, we can unstash changes
git stash pop -q

# Exit if any error codes
let "ERROR = $JSCS_EC+ $ESHINT_EC + $PHPCS_EC + $PHPUNIT_EC"
if [ "${ERROR}" -ne "0" ] then
  echo "Commit aborted."
  exit ${ERROR}
fi
echo "All good!"

You’ll note that the script assumes the developer has jscs, eshint, and phpunit installed. That’s a reasonable expectation; if they’re working on your codebase, they should have their workspace properly set up.

To get this working, all you need to do is locate the name this file pre-commit (just that, without any extensions), and place it in the directory .git/hooks/ (at the top level of your project).

Then you try committing, and voila…

Image for post

Of course, if there are any style errors, the commit would be aborted, and the developer has to fix them before he/she can commit.

But there’s another problem…

You want this file to be the same for all your developers, so of course, you do the smart thing: git add .git/hooks/pre-commit. But Git won’t let you be great…

Image for post

Yes, there it is. You can’t commit files in the .git directory. Sheesh. But for this file to work, it needs to be in the .git/hooks directory.

And another solution..

NPM and Composer have something awesome known as scripts. They’re a lot like hooks. They allow you to do stuff at specific points in your workflow, or whenever you want. You can run a script yourself or have it run automatically whenever an action takes place.

Like Git’s hooks, NPM and Composer have scripts for common actions. Some built-in scripts include:

  • postinstall (NPM) or post-install-cmd (Composer), which is run after someone finishes running npm install or composer install
  • preinstall (NPM) or pre-install-cmd (Composer), which is automatically run just before npm install or composer install

Here are the full lists of NPM and Composer scripts.

So here’s how we can solve this:

  • we’ll put the file in a location we can add to git, say bin/githooks
  • we then set up a Composer/NPM post-install script that copies our Git hook from its resting place (bin/githooks) to its rightful place ( .git/hooks)

Let’s get the right command first. In Bash, the command cp is used to copy files from one location to another. We’ll add two flags:

  • R, so it copies all of the files in our bin/githooks directory over to .git/hooks (just in case we decide to add more hooks later)
  • f, to force the shell to replace any files with the same names with ours

We’ll also add a yes to our standard input. This is because some operating systems execute cp by default with the -i flag. That flag makes the shell prompt you for confirmation before overwriting files. By passing in a yes, we’re ensuring that the copy operation goes through.

So here’s what we come up with.

For composer.json:

{
    //...
    "scripts": {
        "post-install-cmd": [
            "yes | cp -Rf bin/githooks/. .git/hooks/"
        ]
    }
}

For package.json:

{
    //...
    "scripts": {
        "postinstall": "yes | cp -Rf bin/githooks/. .git/hooks/"
    }
}

And voila! Whenever any dev with the project setup runs composer install or npm install, they’ll have the git hook automatically setup. Then they can proceed to committing, and Git will prevent them from committing faulty code.

Git hooks are really powerful for improving your team’s productivity and stepping up your workflow. (And no, I’m not saying they will magically fix all your code problems.) There are lots of use cases. They’re especially useful if you’re part of a large team. I’d love to hear the innovative ways you’ve put them to use. Leave a reply in the comments or tweet at me @theshalvah



I write about my software engineering learnings and experiments. Stay updated with Tentacle: tntcl.app/blog.shalvah.me.

Powered By Swish