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);
}

Похожие записи

NPM и proxy

В посте приводятся команды для конфигурирования npm внутри сети с прокси-сервером