Как подружить Git с приложением на Webpack+React

Иногда бывает полезно отображать некоторую информацию из Git-репозитория прямо в приложении. В статье мы воспользуемся преимуществом встроенной в NodeJS функции execSync и будем показывать в приложении три версии мастер-ветки: версию мастера в текущей ветке, в локальном мастере и удалённую в репозитории.

Предупреждение! Не используйте execSync для пользовательского ввода. Я надеюсь, вы вряд ли будете использовать dev-бандл приложения в качестве production ready, а значит и сторонние данные не попадут в эту функцию. Если каким-то образом вы всё же используете эту сборку в эксплуатации и неведомым образом пользовательский ввод сможет попасть в execSync, пожалуйста, валидируйте введённые данные.

Приступим. Прежде всего, нам нужна функция, которая будет использовать execSync и пытаться исполнять git-команду, которую мы получаем в качестве аргумента-строки.

run-git-command.js:

const execSync = require('child_process').execSync module.exports = function(command) { return String(execSync('git ' + command));
}

Мы используем приведение к строке, потому что execSync возвращает буфер. Эта функция может принимать любую команду в виде строки, которую Git попытается исполнить.

Далее нам нужен объект со всеми нашими переменными окружения для вебпаковского DefinePlugin. В первом приближении я получил нечто подобное:

const branchName = runGitCommand('branch --show-current');
const workingVersion = runGitCommand( `log --format=%B -n 1 $(git merge-base master ${branchName})`
); const runtimeVariables = { 'process.env.WORKING_VERSION': workingVersion,
} const webpackConfig = { //..., plugins: [ new DefinePlugin(runtimeVariables), ], //...,
}

Разберем каждую команду по отдельности.

  • git branch --show-current вернёт название текущей ветки;

  • git merge-base master ${branchName} вернёт хэш коммита, который является последним общим коммитом между текущей веткой и мастером;

  • git log --format=%B commithash вернёт тело и тему коммита по заданному хэшу (также можно получить только тему коммита — %s).

Всё это прекрасно работает, но только ровно один раз за сборку приложения. Затем я наткнулся на runtimeValue («мгновенное значение»/значение среды выполнения):

Возможно задать такие переменные со значениями, которые полагаются на файлы и будут пересчитаны при изменении этих файлов в файловой системе. Это означает, что webpack пересоберёт [приложение], когда отслеживаемые файлы изменятся.

Это вполне подходящее решение описанной выше проблемы: мы получим значение не один раз в начале сборки приложения, а при каждом изменении файла в проекте. Однако есть небольшой недостаток: если разработчик зайдёт в приложение через некоторое время, не внося локально изменений в код проекта, он столкнётся с устаревшей информацией. Я не смог найти лучшего решения этой проблемы, чем запуск пересборки. К тому же по сравнению с получаемыми преимуществами это незначительное неудобство. Если у кого-то в комментариях есть идея, как сделать лучше, буду рад услышать!

Теперь взгляните на следующее приближение:

const runtimeVariables = { 'process.env.WORKING_VERSION': DefinePlugin.runtimeValue(() => { const branchName = runGitCommand('branch --show-current'); const masterVersion = runGitCommand( `log --format=%B -n 1 $(git merge-base master ${branchName})` ); return JSON.stringify(masterVersion) }, true),
} const webpackConfig = { //..., plugins: [ new DefinePlugin(runtimeVariables), ], //...,
}

Мы получим строку с сообщением Merge branch 'X' into 'master' и теперь нужно всего лишь извлечь название ветки X, чтобы показать его в приложении. Настал черёд несложной регулярки:

module.exports = function(string) { const version = string.match(/(rc\/[0-9.]+)/g); return version ? version[0] : null;
}

Так как внутри проекта для предварительных версий используется паттерн rc/x.xxx.x, это регулярное выражение вполне справится со своей задачей. В любом другом случае вам понадобится написать свою регулярку, чтобы вытащить название предварительной версии или ветки.

Соберём всё вместе:

const runTimeVariables = { 'process.env.WORKING_VERSION': DefinePlugin.runtimeValue(() => { const branchName = runGitCommand('branch --show-current'); const command = `log --format=%B -n 1 $(git merge-base master ${branchName})`; const version = extractVersion(runGitCommand(command)); return JSON.stringify(version); }, true),
}

Если вы хотите показать версию вашей мастер-ветки в локальном репозитории, то можно просто выполнить команду git log -n 1 master --format=%B:

const runTimeVariables = { //..., 'process.env.LOCAL_VERSION': DefinePlugin.runtimeValue(() => { const localVersion = extractVersion(runGitCommand('log -n 1 master --format=%B')); return JSON.stringify(localVersion); }, true),
}

И если мы хотим показать версию удалённой мастер-ветки, чтобы знать, когда нужно обновить локальный репозиторий, то нужно с помощью git ls-remote получить список всех указателей — заголовков, тэгов и merge request’ов. Нам интересны только заголовки, неплохо было бы также указать удалённый репозиторий — origin. Чтобы получить список всех своих удалённых репозиториев, можно выполнить команду git remote:

git ls-remote --heads origin

Таким образом мы получим список всех заголовков, но нам нужны только те, что начинаются на rc:

git ls-remote --heads origin rc*

Мы получили заголовки для предварительных версий. Так как стандартная сортировка работает на основе сравнения строк (в нашем случае указателей), а значит 0.100.10 идёт сразу после 0.100.1, то неплохо было бы отсортировать полученные указатели иначе. Например, по дате:

git ls-remote --sort=committerdate --heads origin rc*

Теперь всё в порядке, и нам нужно лишь забрать последний указатель из списка: 

git ls-remote --sort=committerdate --heads origin rc* | tail -n1

Замечательно! Мы получили хэш коммита и его указатель: 

…ccf55a1d        refs/heads/rc/0.100.10

Суммируя вышесказанное, мы можем получить последнюю версию мастер-ветки в удалённом репозитории:

const runTimeVariables = { //..., 'process.env.REMOTE_VERSION': DefinePlugin.runtimeValue(() => { const remoteVersion = extractVersion(runGitCommand('ls-remote --sort=committerdate --heads --quiet origin rc\* | tail -n1')); return JSON.stringify(process.env.REMOTE_VERSION); }, true),
}

Но есть одна проблема. Эта команда использует подключение к удалённому репозиторию, а значит это занимает определённое время. И в некоторых ситуациях (при VPN или плохом соединении) это может занять больше времени, чем обычно; например, 5-10 секунд задержки.

В результате мне пришла идея небольшого фикса. Объявим константу для задержки в секундах и пустую переменную:

const remoteUpdInterval = 300; // в секундах
let remoteUpdTime = null;

Затем внутри обратного вызова для REMOTE_VERSION проверим, что remoteUpdTime — пустое (первый расчёт) или текущее время в миллисекундах — при пересборке превышает предыдущий «таймер», чтобы мы наконец могли обновить значение версии:

const runTimeVariables = { //..., 'process.env.REMOTE_VERSION': DefinePlugin.runtimeValue(() => { if (remoteUpdTime == null || (Date.now() > (remoteUpdTime + remoteUpdInterval*1000))) { remoteUpdTime = Date.now(); process.env.REMOTE_VERSION = extractVersion(runGitCommand('ls-remote --sort=committerdate --heads --quiet origin rc\* | tail -n1')); } return JSON.stringify(process.env.REMOTE_VERSION); }, true),
}

Мы будем получать обновления для удалённой версии мастера каждый раз, когда пересборка будет запущена после заданного периода в 5 минут.

Заключение

Теперь внутри React-приложения можно получить доступ к переменным REMOTE_VERSION, LOCAL_VERSION и WORKING_VERSION, как к любой другой переменной окружения

В нашем приложении мы используем их для двух сценариев:

  1. Разработчик может нажать Shift+V в любой части приложения и увидеть всплывающее окно со всеми тремя версиями.

  2. В настройках есть панель статуса, которая показывает, нужно ли получить свежие изменения из репозитория или просто применить изменения к текущей ветке.

Читайте так же:

  • Бывший сотрудник кредитного союза Нью-Йорка удалила 21,3 ГБ данных в отместку за увольнениеБывший сотрудник кредитного союза Нью-Йорка удалила 21,3 ГБ данных в отместку за увольнение По информации Bleeping Computer, в мае этого года уволенный сотрудник кредитного союза Нью-Йорка в отместку удалила более 21 ГБ данных клиентов и смогла некоторое время просматривать отчетные документы компании. Причем она это сделала спустя двое суток после окончания официальной […]
  • ZeroNights 2022 в календаре каждого безопасникаZeroNights 2022 в календаре каждого безопасника Ежегодная конференция ZeroNights – это квинтэссенция того, чем ИБ-сообщество живет целый год. Хакеры всех мастей – пентестеры, реверс-инженеры, аналитики – собираются на одной площадке, чтобы рассказать и услышать о новых угрозах, возможностях атаки и защиты. Дата: 23 июня […]
  • Что такое цветовое пространство? РазборЧто такое цветовое пространство? Разбор Восприятие цвета — довольно субъективная штука. Кто-то любит более насыщенные и контрастные цвета, кто-то наоборот предпочитает более сдержанные оттенки. Тем не менее, даже в таком субъективном вопросе как восприятие цвета — есть строгая наука. Наверняка, вы слышали такие термины […]
  • Criteo: российские маркетологи увеличили расходы на digital-рекламуCriteo: российские маркетологи увеличили расходы на digital-рекламу Воздействие COVID-19 на потребителей привело к росту онлайн-продаж и ускоренной диджитал-трансформации. К такому выводу пришли аналитики Criteo после проведения исследования состояния рынка диджитал-рекламы 2021.  Исследование показало. Что 48% маркетологов в России существенно […]