git pre-commit Hooks

Posted by ads' corner on Sunday, 2020-10-25
Posted in [Git][Linux]

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:

1
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/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.


Categories: [Git] [Linux]