Предыстория

Как-то пару месяцев назад пришел ко мне в гости в коворкинг поработать удаленно мой давний приятель. Он пишет на Java и использует в своей работе IntelliJ IDEA. Помню, он долго восхищался новой на тот момент фичей встроенного AI Assistant - умением генерировать commit message.

На тот момент я как-то не сильно проникся идеей автогенерации сообщения, потому что я, как человек, который ответственен за процесс code-review в своей команде, с трепетом отношусь к описанию коммита. Прошло немного времени, у меня по работе прилетела задача рефакторинга довольно объемного куска кодовой базы. Причем, эта задача была разбита на подзадачи, связанные с микросервисами. Поэтому, мне надо было писать довольно объемные коммит-сообщения по завершении каждой итерации. И тут я вспомнил про своего приятеля, когда он за минуту редактировал сгенерированное сообщение от AI ассистента и экономил немало времени.

Типа ТЗ

Я тоже иногда периодически пользуюсь IDE от JetBrains, но в тот момент я плотно сидел на NeoVim и кроме эмулятора терминала никакими GUI инструментами не пользовался. Для работы с Git у меня в арсенале консольных инструментов есть Lazygit - прекрасное TUI приложение для оперативной работы с этой системой контроля версий. Поэтому, надо было придумать, каким образом прикрутить ИИ к lazygit и еще очень важным обстоятельством было то, что сообщения должны соответствовать принятым у нас в команде стандартам: содержать в заголовке идентификатор задачи из Jira, списки изменений в секциях: added, deleted, changed, moved.

Поиски подходящего решения

Немного погуглив, я наткнулся на CLI тулзу Aichat с довольно мощной поддержкой разных LLM из коробки. Еще немного погодя, нашел на страничках Github историю о том, как кто-то уже успешно скрещивал эти два инструмента - вот тут, вдруг, кому интересно будет.

Ну все, база для допиливания под себя есть, с этим я двинулся дальше.

Работа напильником

Для начала я создал шаблон сообщения будущих коммитов. Выглядел он в моем случае так:

{summary}

added:
-
changed:
-
moved:
-
deleted:
-

Далее, надо было определиться с LLM, которую надо будет вызывать через aichat. Для начала я решил попробовать бесплатную облачную модель от CloudFlare. Почему именно ее? Да не почему, просто так, не пробовал, не трогал, стало интересно :) Зарегистрировался, получил токен. Весь этот процесс тут описывать не буду, так как он простой и не заслуживает отдельного внимания. Настройки aichat хранятся в файле config.yaml. В MacOS файл находится в ~/Library/Application Support/aichat, в Linux - ~/.config/aichat. Открываем или создаем этот файл в любимом редакторе и добавляем следующее:

clients:
  - type: openai-compatible
    name: cloudflare
    api_base: https://api.cloudflare.com/client/v4/accounts/{client_id}/ai/v1

{client_id} - это ваш идентификатор клиента в CloudFlare.

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

export CLOUDFLARE_API_KEY=$(keyring get cloudflare CLOUDFLARE_API_KEY)

Эта строчка у меня добавлена в .zshrc. Сам токен я храню в keyring. Почему я так делаю, а не сразу прописываю токен при экспорте переменной? Ответ прост - я храню копии конфигурационных файлов в открытом репозитории dotfiles, поэтому, очевидно, что там секреты храниться не должны.

Следующим шагом надо “натравить” lazygit на aichat, чтобы сгенерировать текст коммита. Открываем конфиг lazygit: для MacOS он находится в ~/library/Application Support/lazygit/config.yml, для Linux - ~/.config/lazygit/config.yml и добавляем туда вот такое содержимое:

customCommands:
  - key: <c-a>
    description: Pick AI commit
    command: |
      aichat "Пожалуйста, напишите commit message для следующего коммита, используя результат команды git diff:

        \`\`\`diff
        $(git diff --staged)
        \`\`\`

        **Пример сообщения коммита:**

        Краткое описание изменений.

          added:
            - какие сущности, методы, классы или логика добавлены и т.д.
          removed:
            - какие сущности, методы, классы или логика удалены и т.д.
          modified:
            - какие сущности, методы, классы или логика изменены и т.д.
          moved:
            - какие сущности, методы, классы или логика перемещены и т.д.

        **Выходной шаблон**
        Обязательно используй этот шаблон, вывод которого ниже. Не используй переносы строк, лишние пробелы, лишние символы. Не украшай текст.

        \`\`\`
        $(cat .git/.template)
        \`\`\`


        **Используй предыдущие коммиты для примера:**

        \`\`\`
        $(git log -n 5 --pretty=format:'%h %s')
        \`\`\`

        Напиши свой commit message строго соблюдая шаблон. Пиши на английском. В шаблоне обязательно должны быть заполнены все секции. 
        Не углубляйся в детали, описывай только суть того, что поменялось или добавилось."\
          | fzf --height 40% --border --ansi --read0 --preview "echo {}" --preview-window=up:wrap \
          | xargs -0 -J {} bash -c '
              COMMIT_MSG_FILE=$(mktemp)
              printf "%s" "$1" > "$COMMIT_MSG_FILE"
              ${EDITOR:-nvim} "$COMMIT_MSG_FILE"
              if [ -s "$COMMIT_MSG_FILE" ]; then
                git commit -F "$COMMIT_MSG_FILE"
              else
                echo "Commit message was not saved, commit aborted."
              fi
              rm -f "$COMMIT_MSG_FILE"' _ {}
    context: files
    output: terminal

Примечание: у вас должна быть установлена утилита fzf

Что тут происходит, я, думаю, понятно: при нажатии ctrl+a мы вызываем aichat и передаем промпт модели, используя вывод команд git. Далее, то, что нагенерирует модель попадает в окно fzf и мы можем либо принять то, что она нагенерировала с дальнейшим редактированием (откроется редактор, который у вас определен в переменных среды в переменной EDITOR, либо тот, что по умолчанию).
Ну вот, собственно говоря, и все, это работает. Работает, но с небольшими оговорками:

  • ваш вывод git diff вы передаете на удаленный сервер (это может быть проблемой про NDA);

  • если git diff выводит много чего, то этот вывод может не влезть в кол-во токенов модели.

Со вторым пунктом решение на основе локальной LLM вам, скорее всего, не поможет, а вот с первым - вполне.

Итак, нам потребуется LM Studio. Устанавливаем сие чудесное ПО. Открываем его. Скриншоты с настройками приводить не буду, так как мне кажется, что интерфейс очень понятный, можно сказать, интуитивный :)
Скачиваем модель, которую вы хотите, в моем случае я решил попробовать Deepseek R1 Qwen 3 8B (кто-то мне ее советовал). Загружаем ее. Слева в меню выбираем пункт Developer (с пиктограммой консоли). Там нам надо включить сервер для обработки запросов к модели по HTTP. Включаем рубильничек, проверяем curl http://127.0.0.1:1234 или просто открываем этот адрес в браузере. Там по дефолту будет ошибка {"error":"Unexpected endpoint or method. (GET /)"}, но, тем не менее, статус ответа - 200. Тут нам больше и не надо.

Возвращаемся к aichat, точнее, к его конфигурационному файлу. Добавляем туда:

  - type: openai-compatible
    name: deepseek
    api_base: http://127.0.0.1:1234/v1

Соответственно, весь конфиг у нас выглядит теперь так:

clients:
  - type: openai-compatible
    name: cloudflare
    api_base: https://api.cloudflare.com/client/v4/accounts/01fdd480aaf4e9d99a9ec1609fc00cba/ai/v1

  - type: openai-compatible
    name: deepseek
    api_base: http://127.0.0.1:1234/v1

Подробная документация по возможным конфигурациям тут.

Теперь осталось чуть изменить конфиг lazygit, а именно дописать сюда ключ для выбора локальной модели:

aichat -m deepseek "Пожалуйста, напишите commit message для следующего коммита, используя результат команды git diff:

Готово! Теперь у нас идет генерация commit message при помощи локальной модели.

Вот пример сообщения, который был сгенерирован в репозитории с моими статьями, когда я написал этот текст:

Added two new methods for generating commit messages:

1. Using online Cloudflare LLM (requires internet)
2. Using offline local models like Deepseek via LM Studio

Updated the template documentation to include these new approaches.

Verified that both methods follow our commit message format requirements.

На этом все, надеюсь, было интересно и, возможно, кому-то даже полезно :)

P.S.
Шаблон для сообщения в репозитории с текстами статей такой:

{summary}

{changes description}

Комментарии (9)


  1. aeder
    24.06.2025 19:40

    Замечаетельно! Описания коммитов, сгенерированные нейросетью - читать будет только другая нейросеть.


    1. rsashka
      24.06.2025 19:40

      Если я правильно понял, генерируется описания коммита по заданному шаблону, которое потом используется как черновик, после чего его можно (нужно) править и лишь только после этого окончательно коммитить.

      Это ничем не хуже написания бессмысленной портянки вручную.


      1. BugM
        24.06.2025 19:40

        Если она бессмысленная, то наверно ее просто не надо писать?


    1. n0isy
      24.06.2025 19:40

      Не доказали. А ведь бремя доказательства...

      Вот, к примеру, Ваш комментарий содержит арфаграфичечкие ошибки. Да и мой тоже. Уж лучше пусть llm пишет


  1. Bozaro
    24.06.2025 19:40

    В комментариях к коду и описании коммитов самая ценная информация - ответы на вопросы "зачем это было сделано?" и "почему было сделало именно так?"

    В лучшем случае эту информацию можно достать из тикета, в худшем - только из головы разработчика.


  1. ruomserg
    24.06.2025 19:40

    Что за новая дичь - писать отчет по проделанной работе в commit message ? Commit message должен отвечать на вопрос "Зачем изменения?", а не "Какие изменения?". Кто захочет узнать что именно вы изменили - сделает git diff...

    Как бы это понятнее объяснить... Представьте себе что вы открываете оглавление книги. Одна ситуация - вы видите понятные и короткие названия глав: "Детство в деревне. // C отцом на охоте // Найденыш // etc". Другое когда там начинается: "В деревне Миндюкино было 3999 обывателей, двое умерло, один приехал из города", "От околицы 300 метров до опушки, далее WSW полтора километра к одинокой сосне"... Формально - описано одно и то же. Но если вы читаете нормальное оглавление - вы можете понять что вы уже видели, куда смотреть не имеет смысла (если вы ищете конкретный фрагмент в книге), а куда надо закопаться! А когда коммит-месседж используют как помойку - ну извините!...


    1. shoytov Автор
      24.06.2025 19:40

      я не знаю, какие кейсы у вас были в работе с сообщениями к коммитам, но я часто собирал релизы, и мне было очень полезно видеть не только ссылку на задачу ("зачем это изменение"), но и фактически короткое описание того, что изменилось для общего понимания картины


      1. ruomserg
        24.06.2025 19:40

        У нас были приняты саммари типа таких: "JIRA-XXXX: increased default initial timeout on service connect", "JIRA-YYYY: do not send error details if the receiver signals overflow (error-reporter-client version bump)". Весь смысл в том, что посмотрев на заголовок - вы можете решить: читать этот коммит, или пропустить. Но никакого списка измененых/добавленных/измененных сущностей/файлов разумеется тут нет. Для этого есть git diff, git bisect и остальные команды... Можно спорить - отвечают ли эти заголовки на вопрос "почему?" или "что?" - но это именно высокоуровневое саммари.


        1. shoytov Автор
          24.06.2025 19:40

          ну, как говорится, на вкус и цвет фломастеры у всех разные :)