Skip to content

git pre-commit Hooks

A very common use case for hooks in git is a "pre-commit" hook. This hook is used to verify the to-be-committed data before it is added to the repository.

One important note: hooks are not part of the repository itself. Everyone can install a hook on it's own checkout of a repository, but by default the hook is not there when you clone/checkout the repository. This avoids security problems by executing arbitrary code during "git commit", or any "git" operation.
Because of this implication it is common that developers install a hook from somewhere in the repository into the ".git/hooks" directory. And in addition, the server side (the repository) can run the same checks during "git push", to enforce the rules.

Hooks in git work in a simple way: whatever program or script is run as the hook has to set a return code. If the return code is "0", git proceeds. If it's not "0", git aborts the operation.

 

Back to the hook: the first problem is how to identify the to-be-committed files? "git diff" lists the files:

git diff --cached --name-only --diff-filter=ACM

The options:

  • --staged: lists all files which are stated for the next commit. --staged is an alias for --cached, both versions can be used.
  • --name-only: lists only the filenames, not the entire diff.
  • --diff-filter: selects what kind of files will be included in the diff.
    • A: Added files
    • C: Copied files
    • M: Modified files

The "git diff" above does not select "Renamed" or "Deleted" files. A renamed file might violate naming rules, in this case you have to include this option as well. And a deleted file is about to be removed from the repository, and most likely does not need to be checked.

Let's put this all together into a small shell script which runs as the git hook. In my example I want to enforce two things:

  1. No double spaces in the file
  2. No spaces at the end of the line

Here is the ".git/hooks/pre-commit" example:

#!/bin/bash

set -e

echo "pre-flight checks:"

file_list=`git diff --cached --name-only --diff-filter=ACM`

exit_status=0
while IFS= read -r file; do
    if [[ "$file" =~ ^content/post/* ]];
    then
        echo "checking file: $file ..."
        if grep -q '  ' "$file";
        then
            echo "Double spaces in file!"
            exit_status=1
        fi

        if egrep -q ' +$' "$file";
        then
            echo "Spaces at end of line!"
            exit_status=1
        fi
    fi
done <<< "$file_list"

if [ "$exit_status" -eq "0" ];
then
    echo "Pre-flight check passed!"
    exit 0
else
    echo "There are some errors ..."
    exit 1
fi

The first part identifies the changed files. The loop runs over all changed files, and checks them for the two conditons. The $exit_status variable is set by any error. Finally the last if-then-else part returns an error if there is any problem in the files.

Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

Stephan on :

Hey Andreas, check out pre-commit.com. it's an open source tool built by Yelp for exactly this use case, and many more.
Comments ()

Andreas 'ads' Scherbaum on :

I know about this tool, but I find it oversized for the job. It can do too many things.
Comments ()

Sascha Henke on :

Hi, on this page is an example for a pre-commit hook, when you use Ansible Vault: https://blog.ktz.me/secret-management-with-docker-compose-and-ansible/ I found it quite useful. Sascha
Comments ()

Sebastian on :

"git diff --cached --name-only --diff-filter=ACM" is commonly used but not correct. Let's say you have three changed files and want to commit only one of them. All three would be listed by the command and you'd check all three of them even if only one will end up in the commit. Things get worse if the pre-commit hook is being used for linting. The linter changes have to be "git add"ed again or they won't end up in the commit. If you did "git commit file1", all three would be linted and (if changed) added to the commit that should only have "file1".
Comments ()

Andreas 'ads' Scherbaum on :

Agreed. In my case I always commit all changes, it's a blog and the content is the blog markdown content. Thanks for the explanation.
Comments ()

Add Comment

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
To leave a comment you must approve it via e-mail, which will be sent to your address after submission.
Form options