Introduction
Git tops the list of version control software, allowing developers to collaborate and keep track of changes to source code. Beyond the conventional git workflow of staging changed files, creating commits, and pushing those changes, there is a lot more you can do with Git to 10x your productivity. In this article, we will explore how to leverage Git hooks, a powerful feature available in Git, to automate repetitive tasks and enforce coding standards throughout a repository.
What are Git Hooks
Git hooks are customizable scripts located in the .git/hooks
directory. These scripts are executed when certain events occur. Git hooks are built into Git, meaning they exist if you installed Git. The git hooks directory is populated with sample shell scripts with the file extension .sample
. These scripts have some default code in them. However, you can customize these scripts to adapt them to your project's needs.
Types of Git Hooks
There are two groups of Git hooks: client-side hooks and server-side hooks.
Client-side Hooks
Client-side Git hooks are triggered by events that occur only in the local repository. An example of a client-side git hook is the pre-commit hook which you can set up to execute before any change is committed to your local repository. The client-side hooks available in Git include:
- pre-commit
- prepare-commit-msg
- commit-msg
- post-commit
- pre-rebase
- post-rewrite
- post-checkout
- post-merge
- pre-push
Server-side Hooks
Server-side hooks, as the name implies, are hooks that run on the remote Git repository server. An example of a server-side Git hook is the pre-receive hook. This hook is triggered before any changes are accepted into the remote repository, allowing you to enforce specific checks on incoming code changes. The server-side hooks available in Git include:
- pre-receive
- update
- post-receive
Using Git Hooks in your Workflow
Pre-Commit Hook
The pre-commit hook is a script that Git executes before you commit any code to your repository. Here are some scenarios where you can adopt pre-commit hooks to automate your workflow:
Code formatting
Imagine you had to check out from production real quick to make a hot fix and you forgot to format your code before committing and pushing. Or you can't make up your mind about whether to use single quotes or double quotes, so you end up using both in your code. If that sounds like you, the good news is that you can set up pre-commit hooks that will format your code and enforce a consistent style before you commit new changes to your repository.
Here's how to set up a pre-commit hook that uses Prettier to format code before you make a commit in a Node.js project.
- You should have prettier installed in your project as a dev dependency.
npm install prettier --save-dev
- Create a
.prettierrc
configuration file in your project's root directory and add your settings.
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 80,
"arrowParens": "always"
}
- In the
.git/hooks
directory, open thepre-commit.sample
file. Replace the default code in the file with the code below and rename the file topre-commit
.
#!/bin/sh
# Run prettier on staged files
git diff --name-only --cached --diff-filter=ACM | grep '\.jsx\?$' | xargs ./node_modules/.bin/prettier --write
# Add back the modified/prettified files to staging
git add .
So what exactly does this script do? Glad you asked.
We first tell Git to list the names of all the files that have been staged for the commit. To select the files we want to pass to prettier for formatting, we do grep '\.jsx\?$'
. This filters the list to include only files with the .js
or .jsx
file extension (you can adjust this to match your project's file extensions). We then pipe this filtered list of file names to the prettier
command, which formats each file and writes the changes back to disk. Because the files are formatted, Git detects new changes in them so we have to stage them again by running git add .
- Next, we have to make the pre-commit hook executable. Do this by running this command in your terminal.
chmod +x .git/hooks/pre-commit
Now, whenever you try to commit a new change, Git will execute the pre-commit hook and format your code using Prettier.
Code Linting
You can also use a pre-commit hook to run a linting tool on your code before new changes are committed. This is a great way to catch errors and ensure that only quality is committed to your codebase.
Here's how to set up a pre-commit hook that uses ESlint to lint code before you make a commit in a Node.js project.
- You should have ESlint installed in your project as a dev dependency.
npm install eslint --save-dev
- Create a
.eslintrc.js
configuration file in your project's root directory and add your linting settings.
module.exports = {
env: {
node: true,
es6: true,
},
extends: [
'eslint:recommended',
'plugin:node/recommended',
],
plugins: [
'node',
],
parserOptions: {
ecmaVersion: 2018,
},
rules: {
'no-console': 'off',
'node/no-unpublished-require': 'off',
'node/no-unsupported-features/es-syntax': 'off',
},
};
// sample eslint settings
- Set up your pre-commit hook to run a code-linter by adding this code to the script.
#!/bin/sh
# Find all staged files with a .js, .jsx, or .ts file extension
files=$(git diff --name-only --cached | grep -E '\.(js|jsx|ts)$')
# Lint each staged file with ESLint
if [ -n "$files" ]; then
echo "Linting staged files with ESLint..."
echo "$files" | xargs ./node_modules/.bin/eslint
if [ $? -ne 0 ]; then
echo "ESLint failed. Aborting commit."
exit 1
fi
fi
Just like the previous pre-commit hook we saw, this script starts by finding all staged files that have a .js
, .jsx
, or .ts
file extension using git diff
. If there are staged files with one of those extensions, the script then lints each of those files using ESLint. If ESLint reports any errors, the commit is aborted and an error message is printed in the console. If ESLint reports no errors, the script completes and allows the commit to proceed.
Branch Naming Convention
Some teams maintain a convention for naming branches in a repository. A common convention would be naming a branch with the ticket number and ticket title.
Here's an example of a branch name following this convention: STO-120-signup-bug-fix
. The STO
stands for story. Your team may be following a different convention. You can enforce this convention in your repository using a pre-commit hook.
#!/bin/bash
# Define the pattern to match
PATTERN="^STO-[0-9]+-[a-zA-Z0-9_-]+$"
# Check the current branch name
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
# Check if the branch name matches the pattern
if ! [[ "$BRANCH_NAME" =~ $PATTERN ]]
then
echo "Branch name must match pattern: $PATTERN"
exit 1
fi
Pre-Push Hook
Pre-push hooks are executed before code changes are pushed to the remote repository. A simple yet good scenario for adopting a pre-push hook in your workflow will be to use it to validate commit messages. Following best practices when writing commit messages can distinguish you as a developer who prioritises quality and is keen on detail.
A good commit message should be short and follow this structure:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
A quick break down of a good commit message structure:
- It must have a type. Some commit types include
build:
,chore:
,ci:
,docs:
,style:
,refactor:
,perf:
,test:
- Optional scope: This is to provide some additional context to the commit.
- Description of the commit
- An optional body and optional footer.
To create a pre-push git hook that will enforce commit message best practices in a Node.js project, follow these steps:
- Navigate to the
pre-push.sample
file in the.git/hooks
directory in your project. - Rename the file to
pre-push
without any file extension. - Paste in this code into the file:
#!/usr/bin/env node
const { execSync } = require('child_process');
const commitMsgRegex = /^(feat|fix|docs|style|refactor|test|chore|build)(\(.+\))?: .{1,72}$/;
const validateCommitMsg = (commitMsg) => {
if (!commitMsgRegex.test(commitMsg)) {
console.error(
'\nInvalid commit message format. Please use the following format:\n' +
'type(scope?): subject\n\n' +
'where type can be one of the following:\n' +
'feat: A new feature\n' +
'fix: A bug fix\n' +
'docs: Documentation only changes\n' +
'style: Changes that do not affect the meaning of the code\n' +
'refactor: A code change that neither fixes a bug nor adds a feature\n' +
'test: Adding missing tests or correcting existing tests\n' +
'chore: Changes to the build process or auxiliary tools and libraries\n' +
'build: Changes that affect the build system or external dependencies\n\n' +
'And subject should be no longer than 72 characters.'
);
process.exit(1);
}
};
const main = () => {
const localRef = process.argv[2];
const localSha = process.argv[3];
const remoteRef = process.argv[4];
const remoteSha = process.argv[5];
const commits = execSync(`git log ${remoteSha}..${localSha} --pretty=format:%s`).toString().trim().split('\n');
commits.forEach((commitMsg) => {
validateCommitMsg(commitMsg);
});
};
main();
The pre-push hook uses the Node.js child_process
module to scan the commit messages before changes are pushed to the remote repository. It checks that the commit message is not longer than 72 characters and that it is started with a type which can be any of these types: feat|fix|docs|style|refactor|test|chore|build
. Commit messages that don't conform to the rules will trigger an error and the changes will not be pushed to the remote branch.
Git Hooks and Version Control
It is important to note that the .git/hooks
directory where the hook scripts are stored is not tracked by git. Consequently the hooks that you have set up will not be available to other developers who clone your project.
However, if you want to make the hooks available to other developers working on the project, you need to create a separate directory which will hold the git hooks that your project requires. Here are the steps to follow if you want git to track your hook scripts:
- Create a
hooks
directory in your project's directory. - Add your Git hook scripts to your newly created
hooks
directory. - Create a symbolic link between the
.git/hooks
and the hooks directory in your project's parent directory.
ln -s ../../hooks .git/hooks
- Finally, commit the
hooks
directory to your Git repository.
Now, whenever Git runs a hook, it will look in the hooks
directory in your project.
Note that every developer who clones your project will have to create a symbolic link between the hooks
directory and the .git/hooks
directory on their local machine. This is will ensure that Git looks in the project's hooks
directory when running hooks. You can add this information to your project's README so that developers working in the repository are aware of it.
Conclusion
In this article we explored Git hooks, the customisable built-in scripts that are automatically run by Git when certain actions occur in a repository. We also explored some scenarios where Git hooks can be leveraged to enforce coding standards, catch errors and ensure that only quality changes are committed to the code base.
If you haven't started using git hooks in your project, I encourage to add it to your arsenal of development tools. It is a powerful tool that can increase your efficiency, streamline your workflow and improve the quality of your code base.
Thank you for reading. Feel free to share your thoughts in the comments.
Top comments (11)
Nice article chi, but how can you initialize this git/hook on your project? do I just create a directory for it or what? or is there a command to scafold git hook?
Thank you Emeka. Git hooks are built into every project that has git initialized. So you already have git hooks available in your project. It is inside the .git/hooks folder in the .git directory. Since it hidden, you'll have to set your vscode to unhide hidden folders. You'll see the git hooks then. You can then overwrite the default code in any of the hooks to suit your needs.
Try out husky. It gives you hooks, with less hassle.
Using the VS Code toolbar go to ‘Code’ > ‘Preferences’ > ‘Settings’ and search for ‘exclude’ and you will find the default exclude list. Notice how this can be configured for the current user or the current workspace.
Thank you for reading and sharing that tip.
Nice One Thanks
Thank you Seun.
Great write up sis. But I guess going the git route complicates things too much. I'll rather recommend using Husky hooks. I've been using Husky for some good time now, and it integrates very easily with git.
Husky Is perfect I guess.
Thank you for reading, Andrew. Yes, husky simplifies using git hooks in Nodejs projects. I use it at work too. However, I wanted to delve into the bare bones of git hooks. Thanks for commenting.
Really insightful article
Thank you very much.