Упрощаем написание сообщений коммитов git

В этой заметке я расскажу о том, как автоматизировать процесс написания сообщений для коммитов git.

Постановка задачи copy

Во многих командах приняты различные соглашения об именовании веток и написании сообщений коммитов 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-ветками и создавать в них коммиты. Написание сообщений занимает много времени: после каждой смены ветки приходится либо писать сообщение коммита с нуля, либо рыскать по истории команд в поисках предыдущего коммита, относящегося к нужно фиче.

Что хочу copy

Автоматизировать процесс создания коммита. Не прописывать индекс задачи и название компонента в сообщении вручную, а парсить их из названия текущей ветки.

Решение copy

Для работы с 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:]"
}

Объяснение copy

  • current_branch - функция, возвращающая название текущей git-ветки. Предоставляется плагином git для oh-my-zsh
  • grep -Po "^[a-zA-Z]*-\d*(?=-)" - парсинг индекса задачи
  • tr "[:lower:]" "[:upper:]" - преобразование строчных символов индекса задачи в заглавные
  • grep -Po "(?<=\d-)\w*(?=-)" - парсинг название компонента
  • head -n 1 - взятие первой строки результата. Теоретически название ветки может содержать несколько подстрок, удовлетворяющих описанной выше регулярке. Например, из строки “abc-123-component3-some-feature-2grep спарсит две подстроки: “component3” и “some”. Нас интересует только первая из них
  • tr "[:upper:]" "[:lower:]" - преобразование заглавных символов названия компонента в строчные. На случай, если название ветки будет содержать заглавные буквы :)
  • Документация по Perl-like регуляркам

❗ Для работы Perl-совместимых регулярных выражений (-P) на MacOS нужно использовать утилиту ggrep (GNU grep), вместо дефолтного grep. Поставить её можно командой brew install grep (stackoverflow)

Пример использования copy

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 copy

Для решения этой задачи можно использовать механизм git-hooks. Процесс его настройки для этих целей подробно описан в статье.

Однако мне не нравится данный способ из-за следующих недостатков:

  • Нужно помнить правила работы хуков в различных ситуациях. В противном случае хуки могут поломать ваши сообщения коммитов так, что вы даже не заметите, пока не посмотрите историю коммитов. Конечно, с последовательностью действий “добавить в индекс -> закоммитить -> запушить” вопросов не возникает. Однако в более сложных ситуациях - git rebase, git rebase -i, git commit --amend и проч. - хуки могут сработать не очевидным на первый взгляд образом
  • Написание сообщений коммитов становятся менее явными. При каждом использовании команды git commit -m "..." нужно помнить про, что написанный текст будет отредактирован хуком
  • Хуки неудобно использовать для части репозиториев. Как пишет сам автор, есть два способа включить хуки: “Поместить их в специальную директорию .git/hooks в каждый (!) проект.” и “более элегантный, если подразумевается, что ваши хуки применимы для всех проектов. Нужно добавить в глобальный конфиг Git параметр core.hooksPath, значение которого содержит путь до глобальной папки с хуками, откуда Git будет все тянуть в первую очередь.”

UPDATE 2024-04-27 copy

Через некоторое время после публикации этой статьи я придумал следующий шаг оптимизации: добавил ещё одну функцию, которая:

  1. Принимает на вход описание и тип изменений
  2. Формирующую итоговое сообщение коммита по правилам
  3. Осуществляет коммит

Описание в .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

Если в аргументах не передать описание изменений, коммит не будет осуществлён.

читайте также