Enforce Code Commit Rules Using Git Hooks

Introduction

It is important to ensure the quality and integrity of the code being checked into any version control system. With pipelines automating build and deployment processes, it becomes all the more important to ensure that the code being committed meets the required standards and policies so as not to leave the builds red. Git hooks help in this regard by allowing developers to execute tasks at certain points during git actions like commit, push, etc. In this article, we will look at an example of using one such git hook called pre-commit that runs before you commit the code.

Setup

For this example, we will create a simple TypeScript utility that can calculate the factorial of a given number. Let’s start by creating package.json file inside our project directory by executing the following command:-

npm init -y

I have TypeScript installed globally hence I do not need to install again. In case you do not have TypeScript setup, please run the following command to install it globally. Alternatively, you may also choose to install TypeScript locally by replacing -g with --save-dev in the command below -

npm install -g typescript

Create an src folder and index.ts file inside it. Create TypeScript config file by executing the following command. This will create a tsconfig.json file inside your project directory.

tsc –init

Edit the tsconfig.json file, uncomment outputDir property and update it as follows. This ensures that the TypeScript compiler will compile and spit out js files inside build folder within the project directory.

"outDir": "./build"

Install jest, ts-jest for writing unit tests:-

npm install --save-dev jest @types/jest ts-jest

Create jest config file by executing the following command. This will create a jest.config.js file inside your project directory.

npx ts-jest config:init

Replace the contents of jest.config.js file with the following:-

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  roots: [
    "<rootDir>/src"
  ],
  testMatch: [
    "**/__tests__/**/*.+(ts)",
    "**/?(*.)+(test).+(ts)"
  ],
  transform: {
    "^.+\\.(ts)$": "ts-jest"
  },
};

Setup linter by executing following command. This will also create a .eslintrc.json file inside your project directory.

npm init @eslint/config

Replace the contents of .eslintrc.json file with the following:-

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": "standard-with-typescript",
  "overrides": [],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "ignorePatterns": ["*.test.ts"],
  "rules": {
    "semi": [2, "always"],
    "@typescript-eslint/semi": "off"
  }
}

Update package.json file with following scripts for building TypeScript, running tests and linter.

"scripts": {
  "build": "tsc",
  "test": "jest",
  "lint": "npx eslint src/**"
}

Add .gitignore file to the project with the following content

.vs/
node_modules/
.cache/
build/
.DS_Store

Initialize git repo by executing the following command

git init

Coding and Testing

Update the src/index.ts file. Add a function that calculates the factorial of a given number. We have deliberately introduced a bug in logic so that the tests fail.

export const factorial: (num: number) => number = (num: number) => {
  if (num == 0) {
    return 1;
  } else return num * factorial(num - 2);
};

Create index.test.ts file inside src folder. Add a couple of tests as follows inside the test file.

import { factorial } from ".";

describe("Factorial", () => {
  it("should return 1 for factorial of 0", () => {
    const num = 0;

    const actual = factorial(num);

    expect(actual).toEqual(1);
  });
  it("should return 24 for factorial of 4", () => {
    const num = 4;

    const actual = factorial(num);

    expect(actual).toEqual(24);
  });
});

Execute the tests using the following command. The tests should fail.

npm test

Create a pre-commit hook

You can create hooks as shell script files and put them inside .git/hooks folder inside the project directory. For easier management of hooks, we will use husky. Install husky by executing the following command:-

npx husky-init && npm install

This will create .husky folder inside your project directory with a sample pre-commit hook. It will also set the hooksPath in git config to husky so that all the hooks inside the .husky folders are executed by git. You can verify this by executing the following command:-

git config core.hooksPath

Open the pre-commit file inside the .husky folder and update it as follows. We are instructing git to run linter, execute tests and build the project before executing the commit. If either fails, commit would not happen.

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
npm test
npm run build

Testing pre-commit hook

Let’s stage all the changes.

git add .

Try to commit the changes by executing the following command. It should trigger the actions defined inside the pre-commit hook.

git commit -m "Test hooks"

The commit fails because there is an error identified by linter.

Enforce code commit rules using git hooks

Let's fix the error in index.ts.

export const factorial: (num: number) => number = (num: number) => {
  if (num === 0) {
    return 1;
  } else return num * factorial(num - 2);
};

Stage and try to commit again. The commit still fails because our tests are not passing.

Enforce code commit rules using git hooks

Let’s correct error in our logic by modifying index.ts file.

export const factorial: (num: number) => number = (num: number) => {
  if (num === 0) {
    return 1;
  } else return num * factorial(num - 1);
};

Stage and try to commit again. This time the commit should go through.

Conclusion

Git hooks provide a convenient and powerful mechanism to execute custom tasks before or after various git events. In this article, we saw the example of just one of the hooks viz. the pre-commit hook, that executes before committing the code. Checking commit message format, and scanning staged files for any secrets/keys are some other example use cases where git hooks can come in handy.

References

  • https://git-scm.com/docs/githooks
  • https://typicode.github.io/husky/#/


Similar Articles