DEV Community

Cover image for Writing Custom Git Hooks With NodeJS
Shahar Kazaz
Shahar Kazaz

Posted on

6 3

Writing Custom Git Hooks With NodeJS

Git hooks are a useful tool, especially when working in large teams.
They can help us apply code style and linting standards to our staged files.

In the article, we'll write a few powerful Javascript git hooks that will help us manage our codebase and have a smoother developing experience.

Running The Script 

We are going to run our hooks with the help of Husky 🐶.
After we installed Husky the next thing we need to do is run our node script.
Let's add our script to the package.json scripts section, and use husky to call it:

"scripts": {  
  "hooks:pre-commit": "node ./hooks/pre-commit.js",
  "hooks:pre-push": "node ./hooks/pre-push.js"
},
"husky": {
  "pre-commit": "npm run hooks:pre-commit",
  "pre-push": "npm run hooks:pre-push"
}

That's pretty much it, now let's see some useful implementations of 
pre-commit and pre-push hooks.

Exec.js 

I created an exec.js helper function for my hooks scripts, that wraps shelljs's exec function. 
The exec function spawns a shell then executes a given command within that shell:

const shell = require('shelljs');
function exec(cmd, options) {
const defaultOptions = {silent: true};
let output = shell.exec(cmd, {...defaultOptions, ...(options || {})});
if (options && options.toString !== false) {
output = output.toString();
output = options.trim ? output.trim() : output;
}
return output;
}
exports.exec = exec;
view raw exec.js hosted with ❤ by GitHub

Pre-Commit 📦

1. Branch Names Convention 

Allow to create only branches that have one of the following prefixes: feature|fix|hotfix|chore|tests|automation

const chalk = require('chalk');
const {exec} = require('./exec');
const branchName = exec('git rev-parse --abbrev-ref HEAD', {trim: true});
// check if this branch already exists in the remote
const isInRemote = exec(`git show-branch remotes/origin/${branchName}`, {toString: false}).code === 0;
if (!isInRemote) {
const validBranchPrefix = 'feature|fix|hotfix|chore|tests|automation';
const validBranchesRegex = new RegExp(`^(${validBranchPrefix})\/[\\w.-]+$`);
if (!validBranchesRegex.test(branchName)) {
const msg = `Branch names in this project must adhere to this contract: ${validBranchPrefix}.`
console.log(chalk.bgRed.black.bold(msg));
process.exit(1);
}
}
view raw pre-commit.js hosted with ❤ by GitHub

2. Forbidden Tokens ✋

Who hasn't forgotten to remove a debugger? or an fdescribe in a test? no more!

const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const {exec} = require('./exec');
/* Branch Naming Convention */
...
/* Check Forbidden Tokens */
const FILES_REGEX = {ts: /\.ts$/, spec: /\.spec\.ts$/};
/** Map of forbidden tokens and their match regex */
const forbiddenTokens = {
fit: { rgx: /fit\(/, fileRgx: FILES_REGEX.spec},
fdescribe: { rgx: /fdescribe\(/, fileRgx: FILES_REGEX.spec},
".skip": { rgx: /(describe|context|it)\.skip/, fileRgx: FILES_REGEX.spec},
".only": { rgx: /(describe|context|it)\.only/, fileRgx: FILES_REGEX.spec},
debugger: { rgx: /(debugger);?/, fileRgx: FILES_REGEX.ts},
};
let status = 0;
const gitCommand = `git diff --staged --name-only`;
const stagedFiles = exec(gitCommand).split('\n');
for (let [term, value] of Object.entries(forbiddenTokens)) {
const {rgx, fileRgx, message} = value;
/* Filter relevant files using the files regex */
const relevantFiles = stagedFiles.filter((file) => fileRgx.test(file.trim()));
const failedFiles = relevantFiles.reduce((acc, fileName) => {
const filePath = path.resolve(process.cwd(), fileName.replace('client/', ''));
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, { encoding: 'utf-8' });
if (rgx.test(content)) {
status = 1;
acc.push(fileName);
}
}
return acc;
}, []);
/* Log all the failed files for this token with the matching message */
if (failedFiles.length > 0) {
const msg = message || `The following files contains '${term}' in them:`;
console.log(chalk.bgRed.black.bold(msg));
console.log(chalk.bgRed.black(failedFiles.join('\n')));
}
}
process.exit(status);
view raw pre-commit.js hosted with ❤ by GitHub

Pre-Push 🚀

1. Auto Sync Master

We noticed that developers often forget to update their branches regularly from the remote.

This is a simple but important hook that updates your local branch from the remote master.

const {exec} = require('./exec');
/* Update local branch from origin master */
exec('git pull origin master');
view raw pre-push.js hosted with ❤ by GitHub

2. Forbidden Branches ✋

There are brunches that we don't want their commits ending up in master
such as a staging branch.

We'll make a commit in these branches that will act as a "flag" 🚩.
Before pushing to the remote we will verify that this commit isn't part of the branch being pushed (we will obviously remove this code in the staging branch).

const chalk = require('chalk');
const {exec} = require('./exec');
/* The flag commit hash 🚩 */
const stgUniqCommit = "s76o527m89e9h72619a827s9h038c029o8de";
const currentBranch = exec('git rev-parse --abbrev-ref HEAD');
/* Get all the branch names that have a commit with this hash */
const branchesWithStaging = exec(`git branch --contains ${stgUniqCommit}`);
if (branchesWithStaging.includes(currentBranch)) {
console.log(chalk.bgRed.black.bold(`Your branch contains commits from 'staging' branch.`));
process.exit(1);
}
/* Update local branch from origin master */
exec('git pull origin master');
view raw pre-push.js hosted with ❤ by GitHub

Takeaways 

We saw some useful examples for using git hooks, and how easy you can use Husky and NodeJS to apply policies and prevent bad commits.

Now you can customize these hooks in the best way that suits your project 🥳


Have You Tried Transloco Yet? 🌐

ng-neat introduces Transloco, the internationalization (i18n) library for Angular. It allows you to define translations for your content in different languages and switch between them easily in runtime.

It exposes a rich API to manage translations efficiently and cleanly. It provides multiple plugins that will improve your development experience.
We/I highly recommend you read more about it and check it out!


Introducing Transloco: Angular Internationalization Done Right

🚀 Introducing Transloco: Angular Internationalization Done Right

Translation Files Validation in Angular with Transloco And Husky

Translation Files Validation in Angular with Transloco And Husky

Lazy Load Translation Files In Angular Using Transloco

Lazy Load Translation Files In Angular Using Transloco

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay