Automate Your Workflow With Git Hooks

Anthony Rimet
Anthony RimetFebruary 27, 2024
#github#js

Are your CI tests failing because you forgot to run them locally? Have you misnamed one of your commits? If you work with Git, you've probably already found yourself in a similar situation. With Git Hooks, you can reduce that feeling of going back and forth to correct errors.

Understanding Git Hooks

Git provides a tool named Git Hooks. Hooks let you intercept and enhance your actions, such as commit, push, or merge. If you've never noticed, you can take a look at your project's .git/hooks folder. There you'll find files that serve as examples of hooks.

To see it together, we’re going to initialize a git project:

mkdir git-hooks
cd git-hooks
git init

14 samples are available. You can activate them by removing the .sample extension.

Display Git Hooks samples

The list of available hooks can be found in the git documentation: Git Hooks.

For the purpose of this article, we'll look at how to ensure that our commits are always correctly named (according to our project's naming convention) and that our TypeScript types exist before we can push our code.

Using Git Hooks To Force Commit Message Format

The hook that best matches our first use case is commit-msg. This hook is executed when a commit is created. It is used to check the commit message before it is validated. If the message does not respect the naming convention, the commit will be refused.

To create a commit-msg hook, we will create a file in the .git/hooks folder and give it execution rights.

touch .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg

We want our commit message to start with a ticket number (e.g. '#123: My commit message'). So the hook will be a bash script that parses the commit message and fails if it doesn't respect the right format.

#!/bin/bash

# The commit-msg hook takes one parameter, which is the path
# to a temporary file that contains the commit message.
# So we can read it into a variable using the cat command.
commit_message=$(cat "$1")

echo "Commit message: $commit_message"

# Check if the commit message follows the #XXX: message convention
if [[ ! $commit_message =~ ^#[0-9]+:.* ]]; then
  echo "ERROR: Commit message does not follow the #XXX: message convention."
  exit 1
fi

So let's try our hook.

Commit Message Hook

It works! Now, before pushing the code, we want to make sure that our TS types exist. To do this, we will use the pre-push hook. This hook is executed before pushing the code to the remote server.

touch .git/hooks/pre-push
chmod +x .git/hooks/pre-push

For this pre-push hook, we'll use a bash script to run the TypeScript type check.

#!/bin/bash
npm run typecheck

Pre push hook

Our verifications are perfectly handled!

Sharing Git Hooks With Husky

Here's a problem: how do we share hooks with other team members? By default, .git is not included in the folders to be shared. You can modify the project configuration to move hooks out of the .git folder. We could create a .githooks folder, and link it to our configuration with the command git config core.hooksPath .githooks.

However, this requires configuration and documentation for new team members. Another simpler solution is Husky. It's an npm package for managing git hooks.

npm install --save-dev husky
npx husky init

These commands will create a .husky folder. Inside this folder, you can create a file for each hook you want to use, similar to the Git Hooks method. This approach allows you to version control your hooks and is easy to use.

In a previous article, Arnaud introduced us to conventional comments. This principle standardizes PR comments. Similarly, there are conventional commits. The goal is to make commit messages more readable and easier to follow. Husky will help us implement this convention.

First, we will install the commitlint package, which allows us to verify that our commit messages follow the convention.

npm install --save-dev @commitlint/{cli,config-conventional}
# Configure commitlint to use conventional config
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

And we're going to add a commit-msg hook to check that the commit message follows the convention.

echo "npx --no -- commitlint --edit \\$1" > .husky/commit-msg

We can do the same for checking the TS types before pushing.

Husky commit lint

echo "npm run typecheck" > .husky/pre-push

And now we can let our imagination run wild when it comes to automating tasks with git hooks.

For JavaScript developers like me who aren't big fans of scripting, it's also possible to use Husky with Bun, and with that go through Bun Shell!

Going Further

Before concluding, I wanted to mention GitHooks. It's a site that brings together examples of hooks to help you automate your tasks. You'll find examples for basic git hooks, but also for hooks that are more specific to certain tools, such as Prettier, ESLint and so on.

Among the tools mentioned is pre-commit. It's a framework that lets you use a hook library in different languages, giving you very powerful hooks depending on the task at hand.

Conclusion

Git Hooks are powerful tools for automating your tasks. They can be used to launch scripts at key moments in your workflow. They can be used to check the quality of your code, automate repetitive tasks or standardize commit messages. It's up to you to use them wisely.

I use the pre-commit hook a lot, combining the conventional commit rule with a linter. This way, I make sure my commits are easy to understand, and pull request reviews are simpler because the code is already clean.

Did you like this article? Share it!