Consistency is a very important factor in software development.
But because different developers have different experiences and preferences it takes some time and effort to achieve consistency in projects.
A solution to that are discussed and then tool-enforced rules. Some solutions to that are linters, code formatters, checks on the CI and code reviews.
These are all good practices, but one of the questions that arises is - how and when to have this things running?
With checks on the CI and with code reviews most common scenario is to do these things then a feature/fix is complete and it’s pushed for testing/review.
Automatic code formatters also could live on the CI, but then if they do change any of the code, that has to be pushed back into the repo, and that’s not that good anymore (as the CI shouldn’t contribute to the code).
So linting and code formatting should be done on the developer’s machine. That could be run on each file save - but that may become annoying or even confusing if the code formatting is gonna change the code you’re workin on.
Another option is to run them manually, but in my experience - anything that’s not imposed, is most often ignored.
A better place to run these things are right before you create a commit (if you use an VCS) as at that point you did a small self-contained change that should be able to pass tests. So we can run our checks right before the commit is created. Most VCSes (GIT, SVN, Mercurial, …) support hooks such as pre-commit
and commit-msg
hooks.
pre-commit
hook allows you to run a custom script, and if it exits with a non-zero code than the commit is aborted. This can be used to run tests, linting, automatic code formatting and anything else used to ensure commit/code healthiness.
commit-msg
hook allows you to read and modify the commit message and body.
The problem with GIT hooks is that you can’t just store them in the repo itself and have them magically work. They would need to be symlinked for each new machine.
If your project uses NPM then a very nice solution for that is typicode/husky. This package allows you to define all git-hooks in your package.json, so if a new hook has to be added, removed or changed - all the developers working on the project will get that on the next code fetch.
Many project management tools (e.g. JIRA) have an issues system where each issue is part of the project. So each issue gets an ID of the form TAG-123
where TAG
is the project prefix, and 123
is issue number. This makes each issue unique across entire organization.
Having a reference to the issue in each commit proved many times to be very useful as it provides context to the code without polluting commit messages and a way to filter commits after they get merged into the production/master branch.
In theory it sounds good, in practice different developers have different opinion about importance of doing that, and often simply forget to tag most of the commit. In fact I also sometimes find myself writing meaningless and untagged commit messages.
This can be easily improved by using a commit-msg GIT hook - whenever a commit is created, the hook will check the commit message for the tag, and if one doesn’t exist - it will add it. This script still needs some context on how to get current issue ID. It can be done in few ways, but the easiest one is a naming convention for branches - every branch name starts with the issue ID. This also makes searching checking-out much faster.
These are the steps you have to take to add this to your project:
- Install husky
npm install --save-dev husky
- Create a
scripts
folder and add thecommit-msg.js
file provided at the end of this article - Change
JIRA_TAG
variable value to your project tag - Add a commit-msg to scripts in
package.json
{
"name": "bumbu.me",
"scripts": {
"commit-msg": "node ./scripts/commit-msg.js",
}
}
Now the only change that every developer will have to make is:
Run npm install (the usual) When creating a new branch - prepend it with the issue number (e.g. TAG-234-change-button-style) Now developers shouldn’t have any excuses for why their commits are not tagged (if you agreed as a team to do that)!
#!/usr/bin/env node
import * as fs from 'fs';
const JIRA_TAG = 'TAG';
/* If message title:
* * Doesn't start with square brackets []
* * Doesn't start with Merge branch
* * Doesn't start with Merge pull request
* * Doesn't start with #
* and
* branch name starts with ${JIRA_TAG}-XXX (e.g. TAG-123-branch-description)
* then prepend the issue tag to the commit message
*
* My awesome commit -> [TAG-123] My awesome commit
*/
const startsWithBraces = (str) => str.match(/^\[[^\]]/);
const startsWithMergeBranch = (str) => str.indexOf('Merge branch') === 0;
const startsWithMergePR = (str) => str.indexOf('Merge pull request') === 0;
const startsWithHash = (str) => str.indexOf('#') === 0;
const isInvalidMessage = (str) =>
!startsWithBraces(str) && !startsWithMergeBranch(str) && !startsWithMergePR(str) && !startsWithHash(str);
const tagMatcher = new RegExp(`^${JIRA_TAG}-\\d+`, 'i');
const getIssueTagFromBranchName = (str) => {
const matched = str.match(tagMatcher);
return matched && matched[0];
};
const messageFile = process.env.GIT_PARAMS;
const message = fs.readFileSync(messageFile, { encoding: 'utf-8' });
const messageTitle = message.split('\n')[0];
const branchName = require('child_process').execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).split('\n')[0];
const issueTag = getIssueTagFromBranchName(branchName);
if (issueTag && isInvalidMessage(messageTitle)) {
// Apply the issue tag to message title
const messageLines = message.split('\n');
messageLines[0] = `[${issueTag.toUpperCase()}] ${messageTitle}`;
fs.writeFileSync(messageFile, messageLines.join('\n'), { encoding: 'utf-8' });
console.log(`New message title: ${messageLines[0]}`);
}
When creating a new branch - prepend it with the issue number (e.g. TAG-234-change-button-style)
This same method can be used for many other useful things like:
Spell-check commit messages Check for min/max message length Check for often used meaningless names like Tidy up or Fixes Prepend random emojis to each commit message
use process.argv[2]