Using Git hooks to check your commit code

Secure Commitment

© Photo by Rawpixel on Unsplash

© Photo by Rawpixel on Unsplash

Article from Issue 284/2024
Author(s):

The pre-commit framework lets you automatically manage and maintain your Git hook scripts to deliver better Git commits.

When developing software in a public Git [1] repository, it's recommended to check for common issues in your code prior to committing your changes. Neglecting to do so could lead to your Git repository being cluttered with commits that just fix some minor syntax or style issue. To err is human. Consequently, relying solely on manual checks isn't enough to deliver quality code.

To address this issue, the Git version control system offers a way to start custom scripts when specific actions occur, such as committing changes or merging branches: Git hooks [2]. These hooks are executable (often shell) scripts, stored in the .git/hooks directory of a Git repository. When you create a new repository with the git init command, this directory is populated with several example scripts (Figure 1). Removing the .sample extension from a file name is all that's necessary to enable this hook.

Figure 1: Each Git repository comes with a set of sample Git hooks.

You can use these Git hooks to check for code style on a snapshot that's about to be committed, to edit the default commit message before the commit author sees it, to validate a commit message before allowing a commit to go through, or even to send a notification after the commit process is completed. It's also possible to run scripts before rebasing anything, after a successful git checkout or git merge command, before pushing your commits, and more.

So, if you want to automatically do something before or after one of these Git operations, just create an appropriately named script (without any extension), such as pre-commit if you want Git to run it before committing changes. Put this script in the .git/hooks directory of your Git repository and make it executable. Git will automatically find and run it. It doesn't matter what type of script this is, as long as it's executable. Git hooks can be shell scripts, written in Python, JavaScript, Go, or anything you want.

Most prevalent among users, Git's pre-commit hook allows you to run code linters such as Stylelint [3], Ruff [4], Vale [5], and more, and correct any errors they discover prior to committing your code. But what if you have a complex project where you need Stylelint (written in JavaScript) to check CSS files, Ruff (written in Rust) to inspect Python code, and Vale (written in Go) to validate your documentation? Then, you need to be sure that you can easily install those linters and their language environments.

A Package Manager for Git Hooks

This challenge of managing and maintaining pre-commit hooks for Git repositories has spurred the creation of a dedicated framework, conveniently named pre-commit [6]. The pre-commit framework identifies itself as a "multi-language package manager for pre-commit hooks." All you need to do is list the hooks you wish to use in a YAML [7] file located in your repository. Then pre-commit manages the installation of any hook written in any supported programming language. It automatically installs the necessary programming language environment in an isolated environment (for example, a Python virtual environment) without the need for root access.

You will find pre-commit in the package manager of most distributions, but it is often an outdated version. You can install the most recent release using Python's package manager, pip:

$ pip install pre-commit

Then check whether it's installed correctly:

$ pre-commit --version
pre-commit 3.7.0

A Basic Configuration

To manage your Git hooks with the pre-commit package manager in your Git repository, you need to create a configuration file in the repository's root directory, named .pre-commit-config.yaml. If you don't know where to begin, pre-commit can generate a configuration file with hooks for some basic checks. Just run this command in your Git repository's root directory:

pre-commit sample-config > .pre-commit-config.yaml

The generated configuration file, which looks like Listing 1, is a YAML file with only one mandatory top-level key, repos. The repos key's value is a list of repositories where pre-commit can get the code for the Git hooks.

Listing 1

Default pre-commit Config File

01 # See https://pre-commit.com for more information
02 # See https://pre-commit.com/hooks.html for more hooks
03 repos:
04 - repo: https://github.com/pre-commit/pre-commit-hooks
05   rev: v3.2.0
06   hooks:
07   - id: trailing-whitespace
08   - id: end-of-file-fixer
09   - id: check-yaml
10   - id: check-added-large-files

Listing 1 refers to a single repository, pre-commit's own pre-commit-hooks [8]. The repo key refers to the repository's URL; thus, pre-commit knows which repository to git clone. The rev key holds the version (or Git tag) to install, and the hooks key constitutes a list of mappings describing which hooks to use from the repository.

The trailing-whitespace hook trims all white space from the ends of lines. The end-of-file-fixer hook makes sure files end in a newline and only a newline. The check-yaml hook attempts to load all YAML files to verify their syntax. And the check-added-large-files hook prevents large files (by default files larger than 500KB) from being committed.

Running pre-commit

Before modifying Listing 1 to meet specific needs, I'll test how pre-commit works on a repository. First run the following command in your Git repository's root directory to install pre-commit's Git hook scripts:

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

As indicated by the command's output, it sets up a Git hook script in .git/hooks/pre-commit. This is a shell script that runs the pre-commit command with certain arguments (you can take a peek at the file if you're interested). Thus when you now add files to the index with git add and then run git commit, pre-commit will automatically run the hooks specified in the configuration file (Figure 2).

Figure 2: On running a git commit, pre-commit automatically checks and fixes files you've added.

In the command's output, you see that pre-commit installs the hooks from the repository in its own environment and runs the hook scripts on the added files. While this runs slow the first time due to the installation, pre-commit runs the hooks directly on subsequent commits, which is much faster.

For each of the four hooks that pre-commit runs in this configuration, you get an indication of whether the files pass or fail their checks. The commit is only allowed to go through if all hooks pass their checks.

Some hooks are able to automatically fix a file when it fails a check. For instance, if the trailing-whitespace hook finds white space at the end of a line, it removes it from the file, which is indicated in the output with the message 'Fixing README.md'. Other hook scripts don't automatically fix failures, requiring you to manually make the necessary corrections. This is often the case with code flagged by a code linter.

Regardless if a failure is fixed automatically or manually, you need to re-add the file with git add and commit again. If all hooks now display success, the commit goes through. This approach ensures that all code entering the (local) repository abides by the specific guidelines verified by your pre-commit hooks.

From time to time, it's wise to update all your Git hooks to the latest available version. You can do this with the following command:

$ pre-commit autoupdate
[https://github.com/pre-commit/pre-commit-hooks] updating v3.2.0 -> v4.6.0

This command checks for the latest tag on the default branch of each repository defined in pre-commit's configuration file and updates the rev key to this tag. The next time you run pre-commit, it checks out the new version from the repository and installs it before running the hooks.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Perl: CMS with GitHub

    With its easy-to-use web interface, GitHub can be put to totally different uses than archiving code. For example, Perlmeister Mike Schilli used GitHub to deploy a content management system for simple websites.

  • Perl: Travis CI

    A new service on travis-ci.org picks up GitHub projects, runs new code through test suites, and notifies the owners if the build fails. Its API enables Perl scripts to gather historical build data, including who-broke-the-build tabulations.

  • Tree View

    Complex Git projects sometimes require a better view of the dependencies and branches. Several tools offer GUI options for Git. We take a look at gitk, gitg, git-gui, and GitAhead.

  • Git 101

    When several people collaborate on source code or documents, things can get messy fast. Git provides a quick cure: The distributed versioning system reliably ensures the integrity and consistency of data with minimal effort.

  • Etckeeper

    Etckeeper keeps order in global configuration files and prevents problems with accidentally deleted files.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News