Git Hooks: The Powerful Tool You're Probably Not Using (But Should Be)
Tags: webdev, programming, tutorial, git
Published: 2023-03-26 19:56:51

## 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.

```js
npm install prettier --save-dev
```

* Create a `.prettierrc` configuration file in your project's root directory and add your settings.

```json
{

"trailingComma": "all",

"tabWidth": 2,

"semi": true,

"singleQuote": true,

"jsxSingleQuote": true,

"printWidth": 80,

"arrowParens": "always"

}
```

* 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`.

```shell
#!/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.

```js
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.

```js
npm install eslint --save-dev
```

* Create a `.eslintrc.js` configuration file in your project's root directory and add your linting settings.

```js
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.

```shell
#!/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. 

```shell
#!/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:

```js
<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:
```shell
#!/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.  

```js
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.