Git Hook Management with Pre-commit
Git provides a powerful tool for automation under the hood: hooks. In this tutorial, you will learn about the pre-commit framework that can bring order and structure to your script collection.
Working with Git is not entirely trivial. Even simple things like undoing a step go far beyond a simple undo shortcut but as soon as an entire team works in a repository, the sources of problems grow sky-high.
Git itself handles simple “hard” problems such as merge conflicts or missing updates very well and helps with surprisingly clear error messages. However, when it comes to more complex or “soft” problems, Git is also powerless.
An example: Assume that the team has agreed on a specific commit policy, for example to write commit messages in an understandable, meaningful manner and with specified formatting. Then, for better or for worse, you first have to rely on the reliability of all team members.
This is exactly where Git hooks could come into action and force correct commit messages - or rather encourage them, as I will show later using a concrete example.
What are Git hooks?
Git hooks are essentially something very simple: scripts that are automatically executed upon certain workflow events. The scripts can be written in any scripting language, such as Python for extensive manipulations or as a Bash script for smaller interventions. Workflow events refer to the individual steps such as commit and push on the client side or updates on the server side.
The use is as simple as the concept: The scripts are all in the “./git/hooks” directory under fixed, descriptive names such as “pre-push”. And for convenience, many scripts have example files in the hooks folder by default.
For a simple test, it is enough to simply write a command like “echo foobar” in the pre-push script; The terminal already shows the message “foobar” before the push statement is actually executed.
Different Types of Hooks
Three aspects are relevant for the individual hooks: When exactly are they executed, i.e. triggered by which git command? Which parameters are passed? And does the script run on client side or in the remote repo? Below I show the most important hooks along the usual Git workflow. There are around 28 hooks in total, only 15 are discussed here, which are used more regularly.
Let's start rather unusually with hooks that are triggered by “git am”, i.e. when processing patches via email. There are three hooks here:
- applypatch-msg: gets the name of the file with the commit message as a parameter, is applied before the commit message is set and can change it accordingly.
- pre-applypatch: applied after applying the patch but before committing (e.g. to check status).
- post-applypatch: can be used after commit to distribute notifications.
The “git commit” command calls a total of four hooks:
- pre-commit: runs before the commit message and without parameters - for example to check the changes made in the repo.
- prepare-commit-msg: is started after the delivery of the standard commit message, before starting the commit message editor and receives (up to) three parameters: name of the file with the message, its source and, if applicable, its hash. With this hook it is possible, for example, to include things like issue identifiers in commit messages.
- commit-msg: is called after the message editor and can again check/manipulate the message - this hook is also used for a commit policy below.
- post-commit: a hook that is called in the workflow after the completed commit and is primarily used for notifications - such as an email to the team distribution list that something has been committed.
Using Pre-commit
How Pre-Commit works is pretty simple : the framework is installed in a Git repository and triggered via the “git commit” command. It then executes the installed hooks at the appropriate time, more precisely at a specific stage (commit, push, etc.).
The hooks themselves can be written in almost any language or for different environments, including Conda, Docker, Dotnet, Node, Lua, Perl, Ruby, Rust, Pygrep and of course Python and shell code. Pre-commit usually also takes care of any dependencies that are required for an environment to run the scripts. In some cases, for example with shell script, users have to and can take care of it themselves.
You get even more flexibility by storing the hooks: Although these can be stored locally and directly in the relevant repo, by default they are pulled from their own repository - as is usual with package managers.
Pre-Commit itself specifies Pip, Brew and Conda for installation - however, Snap and Apt packages are also shown under Ubuntu, which are not up to date. I recommend installing via Pip:
pip install pre-commit
It is important to pay attention to any error messages; in our case, the installation was installed in the “~/.local/” directory, which is not in the path. The Apt version, in turn, was installed in the path as usual. If everything works, “pre-commit --version” should show the version number.
Now go to the desired repo. All configuration takes place in the “pre-commit-config.yaml” file, which you can place in the root directory of your repo. To get started, you can create a simple example configuration that uses a few hooks from the pre-commit hooks repository:
pre-commit sample-config > .pre-commit-config.yaml
The sample-config is simply output by default; you have to fill the YAML configuration with it yourself as above. The content is pleasantly simple, here is an excerpt from the demo configuration:
repos:
- repo: https://github.com/Pre-Commit/Pre-Commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
So three pieces of information are needed: the repo with the hooks, the tag for the desired release and the IDs of the desired hooks - more on that later. The hook loaded here would unsurprisingly delete unnecessary spaces at the ends of lines.
Now the configuration is ready and you can install pre-commit:
pre-commit install
With this command, the “pre-commit” file is simply stored in the “.git/hooks” directory - the pre-commit hook is thus activated and executes the pre-commit with the created configuration during “git commit”. And yes, it is a bit confusing that the framework, Git hook and the actual file all have the same name “Pre-Commit”.
Instead of a new commit, hooks can also be triggered manually, for individual files or simply all files, which of course makes sense for new hooks:
pre-commit run --all-files
This means that all hooks specified in the YAML configuration are executed and files are changed if necessary - each confirmed by a nicely formatted output in the form:
`Trim Trailing Whitespace.................................................Passed
- hook id: trailing-whitespace
exit code: 1
- files were modified by this hook`
Fork Hook Repo
The hooks repository basically only needs to contain two things: the hook scripts themselves and another YAML configuration, here the “pre-commit-hooks.yaml” file. The best way to do this is to simply fork the already used hook repo from the pre-commit project - directly via the GitHub interface:
repos:
- repo: https://github.com/IHR-REPO/pre-commit-hooks
rev: v1.0.0
hooks:
- id: trailing-whitespace
-
The configuration in the working repo then requires an update:
pre-commit autoupdate
This sets the rev entry in the pre-commit.yaml to the latest release. If the use of the predefined and forked hooks from your own hook repo works, you can test your own scripts.
Using Your Own Hooks
The hook here is a super simple shell script that simply swaps “foo” for “bar” – in a file “test.sh” in the main directory of the hook repo:
#!/bin/bash sed -i ' s/foo/bar/ ' *
This hook must then be made known to the “pre-commit-hooks.yaml” – for this, you simply copy the part of the “trailing-whitespace” hook seen above and adapt it accordingly:
- id: test
name: test
description: testing stuff
entry: ./test.sh
language: script
types: [text]
stages: [commit, push, manual]
In addition to the name and ID, two entries need to be changed here: “script” is specified as “language” and for “entry” the path to the script must be set, relative to the main directory of the hook repo; Here, for example, test.sh itself is in the main directory.
The other entries for “types” and “stages” are optional; at this point you can specify that only text files are taken into account by the hook and which stages this should be limited to.
Once again it is now necessary to create a new release in the hooks repo. One last step is now missing: The new, custom hook must be added to the pre-commit-config.yaml in the working repo:
repos:
- repo: https://github.com/IHR-REPO/pre-commit-hooks
rev: v1.0.1
hooks:
- id: trailing-whitespace
- id: test
After a final update there is access to the test hook.
pre-commit autoupdate
Very important, although not pre-commit specific: The script must be executable - and of course this also needs to be communicated to git. This works when adding the script using “git add”, but also afterward:
git update-index --chmod=+x test.sh
Final Words
Two repositories, two YAML configurations, releases in one, updates in the other repository, a documentation that actually only uses Python as an example - pre-commit is not exactly intuitive and the first hour can be quite frustrating, but after that it's a great tool.
LHB Community is made of readers like you who like to contribute to the portal by writing helpful Linux tutorials.