Git Pre Commit

Git hooks to enforce uniform style and prevent endless discussions

published: Mon, 05 Dec 2022 estimated reading time: 4 minutes

When you have multiple git repository’s with code one thing that teams often like is a uniform style. This can prevent discussions about style in merge requests and keep the team focussed on the things that really matter (functionality).

In this post we look at how to create a reusable githooks configuration for multiple projects with the help of pre-commit. Pre-commit is a python based tool that hooks into the git hooks system.

Install pre-commit

First install the python package pre-commit so that we are able to use it.

pip install pre-commit

Create a hooks repository

To get started create a new folder and make it a git repository. This way you can use the config over multiple machines. Push it to an origin of you choice (Github, Gitlab, CodeCommit, etc.).

mkdir ~/my_githooks && cd ~/my_githooks && git init 

Create the .pre-commit-config.yaml in the root of the project. In here we will define the hooks we are going to use.

touch .pre-commit-config.yaml

Add the following lines to the config file to have validate json and have it nicely formatted. A list of a lot of available hooks can be found here. There are hooks for all types of linting, preventing the commit of private keys, scanning for security vulnerabilities and if you miss something you can always create your own.

1
2
3
4
5
6
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.3.0
    hooks:
      - id: check-json
      - id: pretty-format-json

Make the hooks repository available for other projects

When we initialize a git repository quite a few files and folders are created. Default git creates a few example hook files under .git/hooks/*.sample . You can add a pre-commit file here manually to use it in 1 project. For this post however we are going to change the configuration variable init.templatedir based on a path to say that the hooks from our repository we just created should be used.

Create ~/.gitconfig_hooks to add the configuration changes.

touch ~/.gitconfig_hooks

In there add the following lines to tell where the hooks can be found.

[init]
    templatedir = ~/Documents/my_githooks

In the .gitconfig file add the following lines to include the just created templatedir for all git projects under ~/Documents/projects (after you do git init on a repository):

[includeIf "gitdir:~/Documents/projects/"]
    path = ~/.gitconfig_testhooks

This configuration will look for an executable file called hooks/pre-commit. Let’s create that file.

touch ~/my_githooks/hooks/pre-commit && sudo chmod +x ~/my_githooks/hooks/pre-commit

In this file we need to say that it should run our githooks from the yaml file. Add the following lines:

#!/usr/bin/env bash
pre-commit run --config "$(git config init.templatedir)/.pre-commit-config.yaml"

Testing the hooks

Create a new repository to test the hooks under ~/Documents/projects/. For example:

mkdir ~/Documents/projects/test_project && cd ~/Documents/projects/test_project && git init 

If you now look at the .git/hooks location you will see a pre-commit file. It contains the same data as the file in ~/my_githooks/hooks/pre-commit. To use the hooks add a json file and add some content to it:

echo "{\"key_one\":1}" > example.json && git add example.json && git commit -m"testing"

As you can see it will run all the hooks we added to the .pre-commit-config.yaml and change the code to the changes required by the hooks. If you add and commit again the code will now be committed because it’s valid.

git add example.json && git commit -m"testing"

Running hooks in different stages

Right now all the hooks we ran where in the stage called commit. We can make this explicit by changing the .pre-commit-config.yaml to the following

1
2
3
4
5
6
7
8
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.3.0
    hooks:
      - id: check-json
        stages: [commit]
      - id: pretty-format-json
        stages: [commit]

Let’s say we have the requirement to always add our ticketnumber before our commit message in a commit. For this we can hook into a different stage called commit-msg. To use this inside our hooks we can add the following to our .pre-commit-config.yaml so that when our branch name starts with JIRA-number it includes JIRA-number: in front of our message.

1
2
3
4
5
6
  - repo:  https://github.com/milin/giticket
    rev: v1.3
    hooks:
      - id:  giticket
        args: ['--regex=JIRA-[0-9]','--format={ticket}: {commit_msg}' ]
        stages: [commit-msg]

To have it work in the global way we use so far we need to create the corresponding file.

touch ~/my_githooks/hooks/commit-msg && sudo chmod +x ~/my_githooks/hooks/commit-msg

The latest commit message is saved inside the .git/COMMIT_EDITMSG" file. This file needs to be added via the script variable if you use this global hook. If you only use pre-commit on 1 repo it’s automatically added. We also need to tell that we want to run only the commit-msg hook, because otherwise every hook will be run. In ~/my_githooks/hooks/commit-msg include the following script:

#!/usr/bin/env bash
pre-commit run --hook-stage commit-msg --commit-msg-filename "$(pwd)/.git/COMMIT_EDITMSG" --config "$(git config init.templatedir)/.pre-commit-config.yaml"

Testing the new hook

Create a branch called JIRA-1234 inside ~/Documents/projects/test_project.

git checkout -b JIRA-1234

Make a change to the example.json file and create a commit. I the following image you can see the outcode if we commit the message four. img.png

Conclusion

In this post you have seen how to use reusable githooks over multiple repositories. You have used them for different stages, validating code and modifying commit messages. Have fun and success applying them in your use-cases.