DEV Community

Cover image for Git Hooks: The Powerful Tool You're Probably Not Using (But Should Be)
Chiamaka Ojiyi
Chiamaka Ojiyi

Posted on

Git Hooks: The Powerful Tool You're Probably Not Using (But Should Be)

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
Enter fullscreen mode Exit fullscreen mode
  • 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"

}
Enter fullscreen mode Exit fullscreen mode
  • In the .git/hooks directory, open the pre-commit.sample file. Replace the default code in the file with the code below and rename the file to pre-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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)]
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
  • 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)

Collapse
 
chubbystrings profile image
Emeka Okwor

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?

Collapse
 
algodame profile image
Chiamaka Ojiyi

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.
Image description

Collapse
 
okpainmo profile image
Andrew James Okpainmo

Try out husky. It gives you hooks, with less hassle.

Collapse
 
wkylin profile image
wkylin.w

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.

Thread Thread
 
algodame profile image
Chiamaka Ojiyi

Thank you for reading and sharing that tip.

Collapse
 
vastrolorde profile image
Seun Daniel Omatsola

Nice One Thanks

Collapse
 
algodame profile image
Chiamaka Ojiyi

Thank you Seun.

Collapse
 
okpainmo profile image
Andrew James Okpainmo

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.

Collapse
 
algodame profile image
Chiamaka Ojiyi

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.

Collapse
 
akinloludavid profile image
akinloludavid

Really insightful article

Collapse
 
algodame profile image
Chiamaka Ojiyi

Thank you very much.