In a previous post I found a way to use native Git hooks mixed with JS, to create a Git hook for verifying conventional commit messages. The end result was JS code, performing the validation logic inside in a “commit-msg” hook, and it was 🔥, but it immediately brought 2 thoughts to mind:
One - This has the potential to grow and by “grow” I mean that now, when it is so intuitive and easy to create a hook in JS, other hooks might follow (running lints, tests, etc.).
Two - The code cries out for packaging and sharing it as a git-hooks package, so that others can install it, import the required hook and execute it in the relevant Git hook.
My way of approaching such a task is to first visualize how using the package will be like in the end. Say that our package name will be “@pedalboard/git-hooks” (given that it is a part of my pedalboard monorepo), I would like it to be used like this:
#!/usr/bin/env node
const {conventionalCommitsValidationHook} = require('@pedalboard/git-hooks');
conventionalCommitsValidationHook.execute();
In the example above, the user will require the “conventionalCommitsValidationHook” module from the “@pedalboard/git-hooks” package, and then call the “execute” method on it, which is the interface I plan for any hook in this package to implement.
I will create the “@pedalboard/git-hooks” package scaffold in the project first. You can read more about creating a package under a Monorepo in previous articles I wrote on the subject, but for now just imagine that we have another package under the “packages” directory, with the relevant configuration, ready to be published.
Next thing is creating the GitHook
interface which has a single method to it, “execute”, in a GitHook.ts
file:
export default interface GitHook {
execute: () => void;
}
This interface should be implemented by any hook that this package will export. Let’s create our first hook, which will be the “conventional commits validation hook” -
Within the packages/git-hooks/src/conventional-commits-validation-hook
directory, here are the tests for this hook:
import fs from 'fs';
import conventionalCommitsValidationHook from './index';
describe('conventional-commits-validation-hook', () => {
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation((msg) => msg);
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation((msg) => msg);
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => code as never);
beforeEach(() => {
jest.clearAllMocks();
});
it('should have an execute method', () => {
expect(conventionalCommitsValidationHook.execute).toBeDefined();
expect(typeof conventionalCommitsValidationHook.execute).toEqual('function');
});
it('should exit on unexpected error', () => {
conventionalCommitsValidationHook.execute();
expect(mockConsoleError).toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(1);
});
it('should be valid when for conventional commits', () => {
const messages: Array<string> = [
'feat: This is a mock message',
'feat(shopping cart): This is a mock message',
'fix: This is a mock message',
'build(release): This is a mock message',
'refactor(some comment): This is a mock message',
'style: This is a mock message',
'chore!: drop support for Node 6',
'docs: correct spelling of CHANGELOG',
'chore(release): publish',
];
messages.forEach((msg) => {
jest.clearAllMocks();
jest.spyOn(fs, 'readFileSync').mockImplementation(() => msg);
conventionalCommitsValidationHook.execute();
expect(mockExit).toHaveBeenCalledWith(0);
});
});
it('should fail validation when not a conventional commit', () => {
jest.spyOn(fs, 'readFileSync').mockImplementation(() => 'not a conventional commit');
conventionalCommitsValidationHook.execute();
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockConsoleLog).toHaveBeenCalledWith(
'Cannot commit: the commit message does not comply with conventional commits standards.'
);
});
});
I’m testing that several different conventional commits are accepted, while another one does not.
Here is the actual hook’s implementation:
import fs from 'fs';
import GitHook from '../GitHook';
const conventionalCommitsValidationHook: GitHook = {
execute: () => {
try {
const conventionalCommitMessageRegExp: RegExp =
/^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\s\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)/g;
let exitCode = 0;
const commitMsgFile = process.argv[2];
const message: string = fs.readFileSync(commitMsgFile, 'utf8');
const isValid: boolean = conventionalCommitMessageRegExp.test(message);
if (!isValid) {
console.log('Cannot commit: the commit message does not comply with conventional commits standards.');
exitCode = 1;
}
process.exit(exitCode);
} catch (error) {
console.error(`Cannot commit: unexpected error occurred: ${error.message}`);
process.exit(1);
}
},
};
export default conventionalCommitsValidationHook;
And this is how the package looks like in the monorepo. You can also check the code on GitHub:
Running the tests, I make sure it all passes and the coverage is sufficient before it’s time to build and use the package.
Inside my project’s .git-hooks
directory, I have a “commit-msg” Git hook, remember?
I start by installing the package on the project root -
yarn add @pedalboard/git-hooks -D
And then I refactor the code of the commit-msg hook to use the hook from the git-hooks package, like so:
#!/usr/bin/env node
const {conventionalCommitsValidationHook} = require('@pedalboard/git-hooks');
conventionalCommitsValidationHook.execute();
And… this is exactly how I planned it to be. Sweet!
Trying to commit something with a non-valid commit message and I get the expected error. We are done :)
The “@pedalboard/git-hooks” package is already available on NPM so feel free to try it out and let me know what you think -
As always, if you have any questions or comments, please leave them in the comments below so that we can all learn from it.
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻
Photo by Kira auf der Heide on Unsplash