В этой заметке я расскажу о том, как автоматизировать процесс написания сообщений для коммитов git.
Постановка задачи 
Во многих командах приняты различные соглашения об именовании веток и написании сообщений коммитов git, позволяющие упростить понимание истории изменений.
В одном из репозиториев, c которым я взаимодействую по долгу службы, используется подход GitHub Flow и приняты следующие соглашения:
Именование веток. Для решения каждой задачи от master-ветки разработчиком создается отдельная feature-ветка. Название такой ветки должно иметь вид: abc-xxx-component-feature_title
abc-xxx
- индекс задачи в таск-трекере. Состоит из:abc
- сокращенное название проектаxxx
- порядковый номер задачи
component
- название компонента, в который вносятся измененияfeature_title
- краткое описание задачи
Сообщения коммитов. Первая строчка сообщения коммита должна иметь вид: ABC-xxx: [?] component: some comment
ABC-xxx
- индекс задачи в таск-трекере?
- тип изменения:+
- добавление функциональности-
- удаление функциональности*
- изменение функциональности
component
- название компонента, в который вносятся измененияsome comment
- описание изменений
Во время работы мне часто приходится переключаться между несколькими feature-ветками и создавать в них коммиты. Написание сообщений занимает много времени: после каждой смены ветки приходится либо писать сообщение коммита с нуля, либо рыскать по истории команд в поисках предыдущего коммита, относящегося к нужно фиче.
Что хочу 
Автоматизировать процесс создания коммита. Не прописывать индекс задачи и название компонента в сообщении вручную, а парсить их из названия текущей ветки.
Решение 
Для работы с git-ом я использую терминальные команды. Подробно о настройках своего терминала уже рассказывал в этом посте. Кратко: zsh + oh-my-zsh + плагины (в частности - плагин git). Поэтому я решил не изобретать велосипед, а использовать возможности, предоставляемые этим набором.
Реализация задуманного умещается в две функции, которые нужно описать в конфиге zsh-а (~/.zshrc
). По сути они являются простыми unix pipeline-ами:
function task_index() {
current_branch | grep -Po "^[a-zA-Z]*-\d*(?=-)" | tr "[:lower:]" "[:upper:]"
}
function component_name() {
current_branch | grep -Po "(?<=\d-)\w*(?=-)" | head -n 1 | tr "[:upper:]" "[:lower:]"
}
Объяснение 
current_branch
- функция, возвращающая название текущей git-ветки. Предоставляется плагином git для oh-my-zshgrep -Po "^[a-zA-Z]*-\d*(?=-)"
- парсинг индекса задачиtr "[:lower:]" "[:upper:]"
- преобразование строчных символов индекса задачи в заглавныеgrep -Po "(?<=\d-)\w*(?=-)"
- парсинг название компонентаhead -n 1
- взятие первой строки результата. Теоретически название ветки может содержать несколько подстрок, удовлетворяющих описанной выше регулярке. Например, из строки “abc-123-component3-some-feature-2”grep
спарсит две подстроки: “component3” и “some”. Нас интересует только первая из нихtr "[:upper:]" "[:lower:]"
- преобразование заглавных символов названия компонента в строчные. На случай, если название ветки будет содержать заглавные буквы :)- Документация по Perl-like регуляркам
❗ Для работы Perl-совместимых регулярных выражений (-P
) на MacOS нужно использовать утилиту ggrep
(GNU grep), вместо дефолтного grep
. Поставить её можно командой brew install grep
(stackoverflow)
Пример использования 
git commit -m "$(task_index): [*] $(component_name): some feature"
Типичная последовательность действий при разработке новой фичи. Помимо описанных выше функций используем алиасы:
gcm # Переключается на master-ветку
gpr # Пуллим изменения из удалённого репозитория
gcb abc-1234-super_service-super_feature # Создаём feature-ветку
# Кодим фичу...
gaa # Добавляем изменения в индекс git-а
gc -m "$(task_index): [*] $(component_name): super feature" # Коммитимся
gpsup # Пушим ветку и изменения в удалённый репозиторий
В итоге получаем коммит с сообщением “ABC-1234: [*] super_service: super feature
”.
Альтернатива. git hooks 
Для решения этой задачи можно использовать механизм git-hooks. Процесс его настройки для этих целей подробно описан в статье.
Однако мне не нравится данный способ из-за следующих недостатков:
- Нужно помнить правила работы хуков в различных ситуациях. В противном случае хуки могут поломать ваши сообщения коммитов так, что вы даже не заметите, пока не посмотрите историю коммитов. Конечно, с последовательностью действий “добавить в индекс -> закоммитить -> запушить” вопросов не возникает. Однако в более сложных ситуациях -
git rebase
,git rebase -i
,git commit --amend
и проч. - хуки могут сработать не очевидным на первый взгляд образом - Написание сообщений коммитов становятся менее явными. При каждом использовании команды
git commit -m "..."
нужно помнить про, что написанный текст будет отредактирован хуком - Хуки неудобно использовать для части репозиториев. Как пишет сам автор, есть два способа включить хуки: “Поместить их в специальную директорию .git/hooks в каждый (!) проект.” и “более элегантный, если подразумевается, что ваши хуки применимы для всех проектов. Нужно добавить в глобальный конфиг Git параметр core.hooksPath, значение которого содержит путь до глобальной папки с хуками, откуда Git будет все тянуть в первую очередь.”
UPDATE 2024-04-27 
Через некоторое время после публикации этой статьи я придумал следующий шаг оптимизации: добавил ещё одну функцию, которая:
- Принимает на вход описание и тип изменений
- Формирующую итоговое сообщение коммита по правилам
- Осуществляет коммит
Описание в .zshrc
:
function glc {
t=""
while getopts "aedn" opt; do
case $opt in
a)
t="[+] "
;;
e)
t="[*] "
;;
d)
t="[-] "
;;
n)
t=""
;;
esac
done
shift $((OPTIND-1))
RED='\033[0;31m'
NC='\033[0m' # No Color
message="$(task_index): $t$(component_name): $1"
if [[ -z "$1" ]]; then
echo "${RED}ERROR: message is empty!${NC}"
else
gc -m "$message"
fi
}
Использование:
glc -a "super feature" # ABC-1234: [+] super_service: super feature
glc -e "super feature" # ABC-1234: [*] super_service: super feature
glc -d "super feature" # ABC-1234: [-] super_service: super feature
glc -n "super feature" # ABC-1234: super_service: super feature
glc "super feature" # ABC-1234: super_service: super feature
Если в аргументах не передать описание изменений, коммит не будет осуществлён.
UPDATE 2025-02-01 
Версия для формата сообщения, содержащего только название задачи:
При невозможности спарсить название задачи из названия ветки коммит не будет осуществлён.
function jt() {
current_branch | grep -Po "^[[:alpha:]]*-\d*" | tr "[:lower:]" "[:upper:]"
}
function glc {
RED='\033[0;31m'
NC='\033[0m' # No Color
message="$(jt): $1"
if [[ -z "$1" ]]; then
echo "${RED}ERROR: message is empty!${NC}"
elif [[ -z "$(jt)" ]]; then
echo "${RED}ERROR: task is empty!${NC}"
else
gc -m "$message"
fi
}
❗ Для работы Perl-совместимых регулярных выражений (-P
) на MacOS нужно использовать утилиту ggrep
(GNU grep), вместо дефолтного grep
. Поставить её можно командой brew install grep
(stackoverflow)