GIT HOOKS

Enforcing Style Conventions

Introduction

Code and style conventions are crucial in team projects and across a company because they promote consistency, making the code easier to read, understand, and maintain. Conventions facilitate smoother on boarding of new developers and enable better collaboration. Pull Requests (PRs) can focus on functional changes rather than getting cluttered with reviews to do with fixing style and aesthetics.

In this article, we will explore how to setup an automated system for enforcing conventions using a combination of git hooks (using the pre-commit framework) and GitHub Actions. We will look at why we would recommend using the pre-commit framework for managing git hooks. As an example, we will examine EmLogic’s git-hooks GitHub repository and how it implements enforcement of a commit message standard.

Git Hooks and pre-commit

Git hooks allow you to run hook scripts on different git events e.g. when you commit or when you push. In your git repository, you can look into the .git/hooks directory to see the templates for the hook scripts you can specify.

If the hook script returns a non-zero exit code, the hook stage will fail. For example, at the “pre-commit“ stage (the stage that runs when make a commit), the commit will not be applied if the hook script returns a non-zero exit code.

What are the limitations of git hooks?

  1. If you want to run multiple hooks at one git stage – e.g. both a formatter for python code and a YAML checker, you would have to implement both checks in the same script file, which can get quite messy if you have many hook functions you want to implement.

  2. If you are working in a team who wants to all use the same hooks, you would have to manually copy the scripts into the .git/hooks/ directory since the .git directory is not version controlled.

The pre-commit framework

Using the pre-commit framework makes managing hooks more simple. It is actually a framework for managing all git hooks stages (pre-commit, commit-msg, pre-push etc.) but is confusingly named the same as only one of the git hook stages.

When you install pre-commit to your git repo (e.g. pre-commit install), it will automatically install scripts in your .git/hooks/directory which point to and execute hooks specified in a .pre-commit-config.yaml file located in your repo root. This way, you can organize all your hook functions in one file, and it is also version controlled, so everyone on the team can use the same git hooks.

Many pre-commit hooks are already implemented and publicly available, and it is easy to add these to your pre-commit hook suite.

Example of using a public hook repository (hooks provided by pre-commit)

				
					  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: check-yaml
      - id: check-merge-conflict
      - id: end-of-file-fixer
      - id: check-executables-have-shebangs
      - id: check-shebang-scripts-are-executable
      - id: no-commit-to-branch
        args: ["--branch", "main"]
				
			

For example, adding the above to your .pre-commit-config.yaml file will run a number of useful checks when you commit code. Here, basic things like white space formatting and line endings are fixed.

It is possible to add hooks from multiple repositories at the same time. For example to add hooks for checking commit messages and applying a commit message template, you simply add another entry to your .pre-commit-config.yaml.

Example of using a public hook repository (hooks provided by EmLogic)

				
					  - repo: https://github.com/EmLogic/git-hooks.git
    rev: v1.2.0
    hooks:
      - id: apply-commit-message-template
      - id: commit-message-checker
				
			

It is also possible to specify local scripts (located in your git repository) to run as hooks in that repository.

Standardizing Commit Messages

There are a number of reasons why a commit message standard makes sense in a project or organization. It means everyone will use the same commit formats, instead of using different formats preferred by each developer.

As noted by the Karma project (depending on your convention), a standard also allows for automatic generation of a change log, since it is easy to filter through which commits are causing functional changes. It is also easy for developers to navigate through the git history, and filter which commits are the ones they want to focus on e.g. ignoring style change commits. A commit standard also allows you a way to link to issue tracking software like Jira, making it easier to track the progress of and review the competition of issues.

In the EmLogic git-hooks repository, we can see an example implementation template of a commit message. Credit should be given to the Karma project, which provided a lot of the inspiration for the EmLogic commit standard.

Enforcing Commit Message Checking

How can we make sure that everyone in the team adheres to the commit message conventions?

In this section we will look at the implementation from the EmLogic’s git-hooks repository.

pre-commit hooks

As we have seen already, using the pre-commit framework allows us to put into version control hooks that we want to run on our git repository.

In the case of a commit message checker, we want to check the commit message while the user commits, to ensure it matches the commit message standard.

This check is simply implemented as a python script, which runs during the commit-msg hook stage. Hooks in the commit-msg stage are passed a file containing the message of the commit. You can see the commit checker script here.

If the script returns a non-zero exit code, the hook stage will fail, and the commit will not be applied. The user will have to commit again with a valid commit message.

In order to help the user write a valid commit message, a message template is provided. For the user to see this template in their commit editor when they commit with git commit (without -m), another hook is provided which places the template into the user’s .git/COMMIT_EDITMSG file. This file is used as a base when the commit message editor is opened. It is possible to set this up manually, but having a hook to do this automatically for every user is a lot more practical.

GitHub Actions

While pre-commit is a great tool, one of its drawbacks is that the user has to remember to install pre-commit (pre-commit install) on their locally cloned repository for the hooks to run. This means if a user forgets to do this, the hooks will not run and checks will be missed. In the case of our example, this means the user could commit messages which are invalid.

To solve this issue, we need to implement checks on the remote repository to ensure that users pushing code to remote have run their pre-commit checks.

In the case of our example, this is achieved using GitHub Actions, since the repository is hosted on GitHub. However, similar things can also be run on other platforms.

				
					name: PR Checks

on:
  pull_request:
    types:
      - synchronize
      - opened
      - reopened

jobs:
  call-commit-checker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # Needed so we can check a range of commits
          fetch-depth: 0
      - uses: ./actions/check_commit_messages/
				
			

Here we see the workflow which runs when you open/reopen or push changes to a PR. It basically calls a local GitHub Action the commit checking script which is used by the pre-commit hooks, but call it for each commit that exists in the PR, to ensure all your commits are correct. If one of the commits are not valid, the check will fail and an error will be shown in the PR. This can be used to block the PR from merging.

Since this GitHub Action is public, it is also possible to call it from another repository workflow, just with the full repository path:

				
					- uses: EmLogic/git-hooks/actions/check_commit_messages@v1.0.0
				
			

The commit message checker is a bit of special case since we want to check a number of commits, so we have to write a custom script to run through each commit. However, for more normal pre-commit checks e.g. python formatting, you can simply run something like pre-commit run --all-files in your GitHub workflow, or use the GitHub – pre-commit/action: a GitHub action to run `pre-commit`. This will run on the state of the most recent commit.

Conclusion

In this article, we have seen how we can enforce a commit message checker on users of a project using a combination of the pre-commit framework and GitHub Actions. pre-commit can check things on the users’ local repositories, but the user has to remember to install the hook scripts with pre-commit install. To ensure that unchecked code is not pushed to the main branch, you can setup a workflow in your remote repository to run the checks again.

An example of a commit message checker is provided in EmLogic’s git-hooks repository, which you are welcome to use yourself or take inspiration from. At the time of writing, this repository provides pre-commit hooks for both checking commit messages, and for applying a git message template when you commit. It also provides a GitHub Action for checking commit messages in your CI pipelines.

 
More Embedded Software Posts