The ultimate guide to Git Hooks

A comprehensive guide around what Git Hooks are and what to use them for, their challenges and how to deal with them, tools to manage their dependencies easier, how to cross-check Git Hooks in when checking pull requests and how to mitigate findings and policy violations.

The ultimate guide to Git Hooks

What are Git Hooks, and why should I use them?

Git Hooks are a Git feature, which allows developers to execute scripts at certain points in their Git lifecycle, like before accepting a commit, before pushing to a remote repository or before accepting a commit message. Git Hooks can be used to  "shift left" and automate tasks and enforce policies throughout the Git workflow.

Git Hooks live in the .git/hooks folder of a repository. Here, we can place scripts with the same name as the Git Hooks they should execute on (max. one per hook). Below, an example which checks the commit message for a certain pattern.

#!/bin/bash

MESSAGE=$(cat $1) 
COMMITFORMAT="^(feat|fix|docs|style|refactor|test|chore|perf|other)(\((.*)\))?: #([0-9]+) (.*)$"

if ! [[ "$MESSAGE" =~ $COMMITFORMAT ]]; then
  echo "Your commit was rejected due to the commit message. Skipping..."   
  exit 1
fi
pre-commit-msg Example taken from here.

Keep in mind, that each script in this folder has to be marked as executable by running chmod +x pre-commit-msg.

You can find more information and a list of available Git Hooks at githooks.com.

What to use them for?

  • Check commit messages: Especially useful, if you use conventional commits. Also, to avoid the famous "WIP" or "fix" commit messages.
  • Check coding style and Linters: A no-brainer. Nothing is more frustrating than going back to your code and fix that trailing space, after the CI pipeline failed because of it.
  • Check for secrets: (Optional) We should check our code for accidentally checked-in secrets, like passwords or connection strings.
  • Build and Test: (Optional) At least before pushing changes to a remote, we could make sure the code builds and the tests pass.

Problems

Can be bypassed

As Git Hooks can be bypassed. For example, with the --no-verfify flag for the git commit command. So we should never rely on their execution but rather see them as a helper to avoid frustration amongst developers, when they find their Pull Request checks failing.

As we can not rely on their execution, Git Hook checks should be repeated when checking the Pull Request on the server!

Not installed to each development environment by default

As Git Hooks live in the .git/hooks folder and the .git folder itself is not part of the version control, scripts that are placed into that folder will not be checked in. So new developers that are cloning the repository won't have the Git Hooks in their local .git folder. Also, remote environments, like GitHub's In-Browser editing or Dev Containers won't have the Git Hooks setup by default.

A common practice around this behavior is placing the scripts into a .githooks folder in the repository root, and then configuring Git to use its Hooks from there with the git config core.hooksPath .githooks command. This allows to check in the scripts into version control, but the command still needs to be run in every new environment and won't make the Hooks available by default on new machines or remote environments.

Dependencies might not be installed

Most scripts for checking coding style violations or commit message patterns aren't as basic as the one from above but use other tools like Prettier, ESLint or editorconfig-checker for Coding Style checks or Gitlint (highly recommended) for commit message policies. These tools might not be installed on new or remote environments.

Managing Git Hooks (and their dependencies) with pre-commit

To deal with the problems from above, there are many tools out there which make working with Git Hooks easier and more straight-forward. Amongst the Node.js and JavaScript community, Husky gained some popularity, as it natively integrates into the package.json file, where app dependencies and development dependencies are managed.

As I deal with projects across different programming languages and frameworks, I was looking for a more generic approach, which can be used with any project. The tool that convinced me the most is pre-commit, as it

  • manages and installs Git Hooks
  • manages and installs dependencies (= tools, that my Git Hooks use)
  • can be run in CI pipelines to check, if Git Hooks haven't been skipped
  • is technology independent (in contrast to popular Husky)

The configuration for pre-commit is stored in a .pre-commit-config.yaml file at the repository root and contains the hooks to configure and the tools that should be executed.

default_install_hook_types:
- pre-commit
- pre-push
- commit-msg

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.0.1
  hooks:
  - name: Check, that no large files have been committed
    id: check-added-large-files

- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
  rev: '2.7.1'
  hooks:
  - name: Check EditorConfig
    id: editorconfig-checker
    alias: ec
    stages: ["pre-push"]

- repo: https://github.com/jorisroovers/gitlint
  rev: v0.19.1
  hooks:
  - name: Lint commit messages
    id: gitlint
  - id: gitlint-ci
    args: ['--commits', 'origin/main..HEAD']
pre-commit-config.yaml

Installing Git Hooks for each developer

The configuration above allows to manage Git Hooks and their dependencies centrally in the repository. But the pre-commit tool itself still needs to be installed on each machine. Once installed, the hooks (and their dependencies) can be installed with a single command.

pre-commit install

Cross-Checking Git Hooks in the CI pipeline

As Git Hooks can be bypassed, it is highly recommended to cross-check them in Pull Request Checks or the CI pipeline. This is another reason, why I like pre-commit so much: The scripts can be executed at-hoc for checking the current Git repository and its commits for violations.

On GitHub, you can check Pull Requests with the following workflow.

name: PR Check
on:
  pull_request:

jobs:
  pr-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Install pre-commit
        run: pip install pre-commit

      - name: Check commits
        run: pre-commit
.github/workflows/pr-check.yaml file for cross-checking Git Hooks in Pull Requests

Mitigate violations

But what to do, when Pre-Push or Pull Request checks find policy violations in our code or commit messages, but the commit is already created and part of the current branch's history?

In case of a committed file that needs to be adjusted, the history of the current branch can be soft-reset to undo all commits that happened on that branch. Don't worry, your work won't be lost. This will put all modifications back to the list of uncommitted changes.

git checkout pr_branch
git reset --soft main

Before creating a commit, we can now adjust the files. Afterward, we can create a new commit without the violation.

git add -A && git commit -m "commit message goes here"
git push --force

To change a commit message afterward, we can follow a comprehensive Guide from GitHub.

Both, resetting the history and changing commit messages is annoying. To avoid that, it is generally recommended to do most checks at the pre-commit level.


☝️ Advertisement Block: I will buy myself a pizza every time I make enough money with these ads to do so. So please feed a hungry developer and consider disabling your Ad Blocker.