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; |
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); | |
} | |
} |
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); |
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'); |
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'); |
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!
Top comments (0)