Git. Добавление имени ветки к сообщению коммита
На работе мы используем трекер задач Youtrack. Он интегрирован с gitlab и отслеживает коммиты. Если в коммите есть id таски, то информация о таком коммите появится в таске. Ветки именуются <feature/id_проекта-id_задачи>
. Ну не будет же программист каждый раз добавлять ссылку в коммит руками...
Само сообщение коммита находится в файле .git/COMMIT_EDITMSG
. Первая строка это заголовок, остальное - тело коммита. Чтобы изменить сообщение коммита достаточно отредактировать этот файл.
Для решения такой задачи отлично подойдет git commit-msg hook. Имя файла (.git/COMMIT_EDITMSG
) git передаёт первым параметром в скрипт. В nodejs это 3 параметр.
С обычными git хуками есть небольшая проблема с переносимостью. Так как код перехватчиков находится в скрытой папке .git/hooks
, то он не попадает в репозитарий и требуется отдельный механизм для развёртывания. С такой задачей отличной справляется npm пакет husky.
npm install husky --save-dev
При установки во время postinstall он прописывет свои перехватчики .git/hooks
, которые в дальнейшем можно задействовать. Аргументы командной строки доступны через перменную окружения HUSKY_GIT_PARAMS
package.json
...
"husky": {
"hooks": {
"commit-msg": "node ./scripts/write-task-name"
}
},
...
write-task-name.js
#!/usr/bin/env node
const fs = require('fs');
const child_process = require('child_process');
const { promisify } = require('util');
const { EOL } = require('os');
const exec = promisify(child_process.exec);
const appendFile = promisify(fs.appendFile);
const timeToWrite = 5000;
const branchContract = /^(feature|bug)\/(isp6|vepp)-[0-9]{1,4}/;
const taskContract = /(isp6|vepp)-[0-9]{1,4}/;
const commitEditmsgFile = process.env.HUSKY_GIT_PARAMS || process.argv[2]; // file '.git/COMMIT_EDITMSG'
cleanup();
main();
async function getCurrentBranch() {
const branchOutput = await exec('git symbolic-ref --short HEAD');
if (branchOutput.stderr) {
throw new Error(stderr);
}
return branchOutput.stdout;
}
function getTaskFromBranch(branchName) {
if (!branchContract.test(branchName)) {
console.log('Имя ветки не соответствует шаблону "{type}/isp6-{number}"');
throw new Error('Unsupported branch name');
}
const [_project, task] = branchName.split('/');
return task.replace(/\s+/g, '');
}
function existTaskInFile(file) {
const message = fs.readFileSync(file, 'utf8');
const withoutComments = message.split(EOL).filter(l => !/^#/.test(l)).join('');
return taskContract.test(withoutComments);
}
function writeTaskToBodyInFile(task, file) {
return appendFile(file, EOL + 'Youtrack task: ' + task, 'utf8');
}
function writeTaskToTitleInFile(task, file) {
const message = fs.readFileSync(file, 'utf8');
const lines = message.split(EOL);
lines[0] += ' (#' + task + ')';
const newLines = lines.join(EOL);
fs.writeFileSync(file, newLines, 'utf8');
}
function cleanup() {
setTimeout(() => {
console.log('Таймаут commit-msg hook ', timeToWrite);
process.exit(1);
}, timeToWrite);
}
async function main() {
let task = '';
let branchName = '';
if (existTaskInFile(commitEditmsgFile)) {
console.log('В commit сообщении уже есть id задачи');
process.exit(0);
}
try {
branchName = await getCurrentBranch();
task = getTaskFromBranch(branchName);
} catch (error) {
console.log('Не удалось получить имя задачи', task, 'из ветки', branchName);
process.exit(0);
}
try {
writeTaskToTitleInFile(task, commitEditmsgFile);
} catch(err) {
console.log('Не удалось записать имя задачи', task, 'в commit-msg файл', commitEditmsgFile);
console.error(err);
process.exit(1);
}
process.exit(0);
}