Иногда бывает полезно отображать некоторую информацию из 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
, как к любой другой переменной окружения
В нашем приложении мы используем их для двух сценариев:
Разработчик может нажать Shift+V в любой части приложения и увидеть всплывающее окно со всеми тремя версиями.
В настройках есть панель статуса, которая показывает, нужно ли получить свежие изменения из репозитория или просто применить изменения к текущей ветке.
Комментарии (5)
inoyakaigor
18.02.2022 12:09Я иногда держу несколько запущенных копий моего приложения и чтобы не путаться в терминалах где какая версия запущена я написал плагин к Вебпаку который выводит после каждого ребилда информацию о текущем времени, ветке и теге и под капотом он делает примерно то, что и вы описали в статье, только максимально упрощённо
Alexandroppolus
18.02.2022 20:36Мы используем приведение к строке, потому что execSync возвращает буфер.
const result = execSync(command, {encoding: 'utf8'});
Вообще, для работы с гитом есть, например, https://www.npmjs.com/package/simple-git
nebsehemvi Автор
18.02.2022 22:05Да, уже после публикации заметил, что можно было к строке привести так.
Век живи - век учись :)
Спасибо за ссылку.
Да, если брать готовую библиотеку, все равно нужно дружить уже библиотеку с вебпаком. Плюс лично мне проще записать команду строкой, чем "матчить" свои знания git с API автора.
skeevy
Можно пойти еще дальше и принудительно обновлять ветки, если локальный мастер отстает от ремоута :)
nebsehemvi Автор
Да, это хорошая идея)
Можно даже пойти дальше и автоматически свежий мастер подмерживать в рабочую ветку, если нет конфликтов