DEV Community

Karel Křemel
Karel Křemel

Posted on • Originally published at karelkremel.com

Making Git Hooks Persistent - No More Manual Setup

During a recent interview, I had an interesting conversation about coding standards and building pipelines. The company I was speaking with took developer responsibility seriously—they had linters, quality checks, and a dedicated team. But there was something that didn't quite add up.

They were trying to automate everything, yet developers still had to run linters manually before committing code. When I asked why they weren't using pre-commit git hooks, they explained: "Oh, we have pre-commit hooks that run the linter, but developers need to activate them manually after cloning the repository."

That's when it clicked. They had git hooks, but they weren't persistent. Every time someone cloned the repo, they got the default sample hooks instead of the team's carefully crafted automation.

I realized they didn't know about a straightforward configuration that could make their hooks work automatically for everyone immediately after cloning.

The Root Issue

The problem is that Git doesn't track the .git/hooks/ directory. When you clone a repository, you get the default sample hooks - not the team's custom automation. This forces teams into workarounds that defeat the purpose of automation.

The Solution: Custom Hook Paths

Git lets you set a custom location for hooks using the core.hooksPath setting. Pointing it to a tracked directory in your repository makes your hooks become part of the codebase.

Step 1: Create Your Hook Directory

Create a directory in your repository to store hooks:

mkdir -p scripts/git-hooks
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure Git to Use Your Custom Path

Add this to your repository's .gitconfig file (create it, if needed):

[core]
    hooksPath = ./scripts/git-hooks/
Enter fullscreen mode Exit fullscreen mode

Commit this file to your repository.

Step 3: Create Your Hooks

Here's a simple example of a pre-commit hook that runs PHP linting:
scripts/git-hooks/pre-commit

#!/usr/bin/env bash

REPO=$(git rev-parse --show-toplevel)

# Get list of staged PHP files
FILES=$(git status --porcelain | grep '^[AM] .*\.php$' | cut -c 3- | tr '\n' ' ')

if [ -n "$FILES" ]; then
    $REPO/source/vendor/bin/php-cs-fixer --no-interaction --config=$REPO/.quality/php-cs-fixer.php fix $FILES
    git add $FILES
fi
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x scripts/git-hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

Step 4: Test and Commit

Test your hook:

git add .
git commit -m "Add persistent git hooks"
Enter fullscreen mode Exit fullscreen mode

Advanced Example: PHP Project with Multiple Tools

I developed this solution years ago, and it has since grown significantly. It now supports more than just PHP — it also checks for the availability of required tools and offers to install them if missing.
Don't get me wrong, it is far from perfect. There's still room for improvement. It assumes a Debian-based Linux environment, doesn't run unit tests, and has other limitations. However, it's sufficient for BiStro, the project for which it was initially built.

#!/usr/bin/env bash

REPO=$(git rev-parse --show-toplevel)
export COMPOSER_HOME="$PWD/.composer"

ECS="$REPO/source/vendor/bin/ecs"
ECS_CONFIG="$REPO/.quality/ecs.php"
PHPCS="$REPO/source/vendor/bin/php-cs-fixer"
PHPCS_CONFIG="$REPO/.quality/php-cs-fixer.php"
LATTE="$REPO/source/vendor/bin/latte-lint"
PHPSTAN="$REPO/source/vendor/bin/phpstan"
PHPSTAN_CONFIG="$REPO/.quality/phpstan.neon"

FILES=` git status --porcelain | grep '^[AM] .*\.\(php\|md\)$' | cut -c 3- | tr '\n' ' '`

exec < /dev/tty

function install_tools()
{
    sudo apt -qq -y install composer
    composer install
    composer require --dev symplify/easy-coding-standard
    composer require --dev friendsofphp/php-cs-fixer
    composer require --dev phpstan/phpstan
}

if [ ! -x $ECS ] || [ ! -x $LATTE ] || [ ! -x $PHPCS ] || [ ! -x $PHPSTAN ]; then
    while read -p "No validation tools found, should I install them? (Y/n) " yn; do
        case $yn in
            [Yy] ) install_tools;;
            [Nn] ) exit 1;;
            * ) echo "Please answer y (yes) or n (no):" && continue;
        esac
    done
fi

if [ -x $ECS ] && [ -x $LATTE ] && [ -x $PHPCS ] ; then
    if [ -n "$FILES" ]; then
        echo "################################## PHP LINTER ##################################"
        $ECS check  --config=$ECS_CONFIG --fix $FILES
        $PHPCS --no-interaction --config=$PHPCS_CONFIG fix $FILES
        git add $FILES
    fi

    echo "################################# LATTE LINTER #################################"
    $LATTE $repo_root/source/templates
    git add < git status --porcelain | grep '^[MA] .*\.latte$'

    echo "################################ STATIC ANALYSIS ###############################"
    $PHPSTAN analyse --no-interaction --memory-limit 512M -c $PHPSTAN_CONFIG
fi

exec <&-
Enter fullscreen mode Exit fullscreen mode

Top comments (0)