dartyushin.techОткрыть меню
← к книге
глава 0710 минут

Commit Generation: как LLM описывает изменения

В предыдущих главах все рассмотренные AI-функции помогали разработчику в процессе работы с кодом: подсказывали с помощью LLM, пытались предсказать следующее изменение в произвольном места проекта и радектировали выбранный участок кода по явному запросу. Все эти сценарии все еще только помогают - разрабочик продолжает сам формулировать задачу, выбирает алгоритм решения и доставляет полученный код до продакшена. Последний шаг связан с созданием некоторого commit в системе контроля версий (как правило, Git). Этот комит содержит в себе набор изменений, которые решают поставленную задачу, и он сопровождается commit message - коротким описанием того, что произошло в этих изменениях. Например,

Fix payment retry handling

Позже это сообщение можно увидеть в истории Git, pull request, git blame или release notes. Хороший commit message позволяет понять смысл изменения, не открывая сам diff.

Но писать такие сообщения приходится постоянно. Разработчик уже закончил задачу, несколько раз перечитал код, запустил тесты и подготовил изменения. В этот момент ему остается еще раз восстановить основную мысль и сформулировать ее в одной строке. Чаще всего на это не остается ни времени, ни желания. Поэтому нередко в git history можно встретить комиты с сообщениями вида: fix, update или some changes.

Автоматическая генерация commit messages кажется естественным завершением работы AI-ассистента: если система уже помогала писать код - она может помочь и описать получившееся изменение.

Попытки генерировать сообщения для коммитов появились сильно раньше современных языковых моделей. Одним из ранних подходов был ChangeScribe, предложенный в 2014 году [1]. Система сравнивала две версии Java-проекта, находила структурные изменения в классах и методах, а затем собирала их описание по заранее подготовленным правилам. ChangeScribe могла определить, что разработчик добавил метод, изменил сигнатуру или вызвал новую функцию. Система старалась описывать изменение на уровне структуры программы, а не просто перечислять добавленные строки.

Позже генерацию commit messages стали рассматривать как задачу машинного перевода: code diff → natural language. Нейросеть получала на вход diff и училась воспроизводить сообщение, которое написал разработчик. Для этого использовались encoder-decoder-архитектуры и attention [2].

Параллельно развивался retrieval-подход. Вместо генерации система могла найти в истории похожее изменение и использовать его commit message. Например, NNGen представлял diff в виде набора признаков, искал похожие коммиты и возвращал сообщение одного из них [3].

В результате сформировались три основных направления: анализ кода и шаблоны, поиск похожих коммитов и генеративные модели. Современные LLM идут в последнем. Им можно передать diff вместе с обычной текстовой инструкцией и получить сообщение без обучения отдельной модели для конкретного языка программирования или проекта. Но предыдущие подходы не исчезли. Структурный анализ помогает объяснить модели, что изменилось в коде, а поиск по истории репозитория - подобрать стиль и терминологию итогового сообщения.

Со стороны разработчика функция выглядит просто:

изменения в Git

Generate Commit Message

готовое сообщение

Кажется, что и внутри достаточно трех шагов:

Получить подготовленные для коммита изменения можно с помощью команды:

git diff --staged

После этого diff добавляется в prompt:

Generate a concise commit message for the following changes.

Return only the commit message.

<DIFF>
...
</DIFF>

Для небольших и очевидных изменений такая схема действительно работает.

Предположим, разработчик увеличил тайм-аут:

-const requestTimeout = 1_000;
+const requestTimeout = 3_000;

Модель может написать:

Increase request timeout to 3 seconds

Почти вся необходимая информация содержится непосредственно в diff. Изменение небольшое, его смысл понятен из названий переменных, а сообщение не требует дополнительного знания о проекте.

Но так происходит не всегда.

Diff показывает изменение кода, но не его смысл

Рассмотрим функцию, которая загружает пользователя перед созданием сессии:

export async function findUser(id: string) {
    return userRepository.findById(id);
}

Разработчик заменяет используемый метод репозитория:

 export async function findUser(id: string) {
-    return userRepository.findById(id);
+    return userRepository.findActiveById(id);
 }

Передадим этот diff модели. Она может предложить:

Update user repository method

Или более конкретный вариант:

Use findActiveById when loading users

Оба сообщения правильно описывают код. Один вызов действительно был заменен другим.

Но полезный commit message мог бы выглядеть иначе:

Prevent inactive users from logging in

Между этими формулировками находятся несколько уровней понимания изменения:

Replace findById with findActiveById

Use active-user lookup in findUser

Do not return inactive users during authentication

Prevent deactivated accounts from receiving sessions

Git diff хорошо показывает первые уровни: измененные строки, названия функций и структуру кода.

Поведение системы иногда можно восстановить по окружающему коду и тестам.

Но намерение разработчика может вообще отсутствовать в diff. Причина изменения находится в задаче, названии ветки, описании бага или просто в голове автора.

Поэтому commit message generation — это не только суммаризация текста:

как изменился код

что изменилось в системе

зачем это было сделано

LLM должна не просто пересказать diff, а восстановить смысл изменения. Для этого ей необходим дополнительный контекст.

Контекст задачи

Наиболее прямое описание намерения обычно находится в задаче.

Предположим, текущая ветка связана с Jira-задачей:

BILL-1842

Inactive users can still receive a session after their account
has been deactivated.

Теперь системе доступны два источника:

Jira:
неактивные пользователи продолжают получать сессии

Git diff:
findById заменен на findActiveById

Добавим задачу в prompt:

Task:

BILL-1842
Inactive users can still receive a session after their account
has been deactivated.

Generate a concise commit message for the following changes.

<DIFF>
...
</DIFF>

Модель может написать:

Prevent deactivated users from receiving new sessions

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

Схема становится шире:

Однако нельзя просто пересказать текст задачи.

Предположим, в Jira написано:

Block all operations for deactivated accounts.

А текущий commit меняет только создание сессии.

Если модель полностью доверится задаче, она может написать:

Block all operations for deactivated accounts

Такое сообщение преувеличивает фактически выполненную работу.

Задача описывает желаемый результат. Diff показывает реализованную часть:

           Jira task
      что хотели сделать

             Git diff
      что действительно сделали

         commit message

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

Другие источники контекста

Jira-задача доступна не всегда. Кроме того, одной задачи недостаточно, чтобы определить формат сообщения, терминологию проекта и границы конкретного коммита.

Поэтому система может использовать дополнительные источники.

Название ветки

Часть намерения часто находится в branch name:

fix/disable-inactive-user-login

Даже без Jira оно помогает перейти от технического описания:

Use findActiveById when loading users

к изменению поведения:

Prevent inactive users from logging in

Схема расширяется:

git diff ───────────┐
branch name ────────┼──→ prompt → LLM
Jira task ──────────┘

Но название ветки является слабым сигналом.

Иногда оно хорошо описывает изменение:

fix/payment-retry-timeout

Иногда содержит только номер задачи:

BILL-1842

А иногда не несет почти никакого смысла:

denis/test

Поэтому branch name полезно передавать модели как дополнительный контекст, но не как готовое описание коммита.

Правила репозитория

Даже правильно поняв изменение, модель должна оформить сообщение в принятом формате.

В одном проекте пишут:

Prevent inactive users from logging in

В другом используют Conventional Commits:

fix(auth): prevent inactive user login

В третьем обязательно добавляют номер задачи:

BILL-1842 Prevent login for deactivated accounts

Правила можно хранить, например, в специальном файле репозитория:

commit-message:
  language: en
  format: conventional-commits
  max-subject-length: 72
  include-issue-key: true

  allowed-types:
    - feat
    - fix
    - refactor
    - test
    - docs
    - chore

  rules:
    - use lowercase after the colon
    - do not add a period
    - describe behavior, not changed files

Название и формат такого файла являются внутренним решением конкретного продукта. Это может быть policy.yaml, AGENTS.md, настройка IDE или часть системного prompt.

После добавления правил результат становится предсказуемее:

fix(auth): prevent inactive user login

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

{
  "type": "fix",
  "scope": "auth",
  "summary": "prevent inactive user login"
}

А итоговую строку собрать программно:

fix(auth): prevent inactive user login

В этом случае задачи разделяются:

LLM:
понимание изменения

программный код:
проверка и форматирование

История Git

Правила не всегда записаны явно. Часто они существуют только в истории репозитория:

fix(auth): reject expired refresh tokens
fix(auth): clear sessions after password reset
feat(auth): add device confirmation

Эти сообщения можно добавить в prompt как few-shot-примеры:

Examples from this repository:

1. fix(auth): reject expired refresh tokens
2. fix(auth): clear sessions after password reset
3. feat(auth): add device confirmation

Generate a message for the current changes using the same style.

Но просто взять последние несколько коммитов недостаточно.

Предположим, недавно менялась только документация:

docs(api): update authentication examples
docs(readme): clarify local setup
chore(docs): fix broken links

Такие примеры показывают формат, но плохо помогают описать изменение бизнес-логики.

Полезнее искать коммиты, похожие на текущий:

текущий diff

файлы, символы и термины

поиск похожих изменений

подходящие commit messages

few-shot prompt

Для изменения в модуле аутентификации можно повысить вес коммитов:

из того же каталога; затрагивающих похожие методы; содержащих слова session, user, active; изменяющих похожие тесты.

Здесь генерация commit message пересекается с retrieval и ранжированием контекста. Система должна выбрать не любые примеры, а те, которые передадут модели подходящий стиль и терминологию.

В результате prompt может содержать сразу несколько слоев:

Repository policy:
- Use Conventional Commits.
- Keep the subject under 72 characters.

Task:
BILL-1842: Inactive users can receive sessions after deactivation.

Branch:
fix/disable-inactive-user-login

Similar commit messages:
- fix(auth): reject expired refresh tokens
- fix(auth): clear sessions after password reset

Changes:
- createSession now uses findActiveById.
- Inactive users are treated as invalid credentials.

Результат:

fix(auth): prevent inactive user login

Большой diff

До сих пор мы рассматривали небольшой атомарный commit.

В реальном проекте staged changes могут включать десятки файлов:

src/auth/createSession.ts
src/repositories/userRepository.ts
src/repositories/postgresUserRepository.ts
src/auth/createSession.test.ts
src/generated/api.ts
package-lock.json
snapshots/auth.snap

Внутри находятся:

изменение бизнес-логики; новый метод интерфейса; реализация запроса; тесты; сгенерированный клиент; lock-файл; * snapshots.

Можно передать модели весь diff целиком:

git diff --staged

      prompt

Но технические изменения начинают конкурировать с основным смыслом.

Lock-файл может занимать тысячи строк, хотя фактически разработчик только обновил одну зависимость:

-"@company/auth-client": "1.4.0"
+"@company/auth-client": "1.5.0"

Сгенерированный API-клиент может повторять изменение, которое уже описано в исходной схеме. Snapshots содержат большие фрагменты данных, а бинарные файлы вообще не имеют полезного текстового diff.

Поэтому перед генерацией изменения полезно нормализовать:

raw diff

удаление бинарных файлов

сжатие lock-файлов

понижение веса generated-кода

группировка snapshots

normalized changes

Например, вместо полного package-lock.json модели можно передать:

Dependency updated:
@company/auth-client 1.4.0 → 1.5.0

Вместо нескольких snapshots:

3 authentication snapshots updated.

Вместо большого generated-файла:

Generated API client updated after changing api.yaml.

Технические файлы не всегда нужно полностью удалять. Обновление зависимости или сгенерированного клиента может быть основной целью коммита. Но такие изменения необходимо представить компактно и не позволить им вытеснить бизнес-смысл.

После фильтрации связанные изменения можно сгруппировать:

Group 1. Authentication behavior
- createSession uses findActiveById.
- Inactive users are rejected.
- A test covers the new behavior.

Group 2. Repository support
- UserRepository exposes findActiveById.
- PostgresUserRepository filters by active status.

Group 3. Technical artifacts
- Authentication snapshots updated.

Затем выделить главное:

Primary change:
Prevent inactive users from receiving sessions.

Supporting changes:
- Add active-user lookup to UserRepository.
- Cover the behavior with a test.
- Update snapshots.

Вместо сотен строк diff модель получает компактное представление изменения:

raw diff

изменения по файлам

связанные группы

основное изменение

поддерживающие изменения

LLM

Группировка также помогает обнаружить, что в commit попало несколько независимых задач.

Например:

1. Prevent inactive users from creating sessions.
2. Upgrade the database client.
3. Fix documentation.

Вместо размытого сообщения:

Update authentication and dependencies

система может предложить разделить изменения на несколько коммитов.

В этом случае AI помогает уже не только придумать текст, но и определить правильные границы commit.

Полная схема

В начале реализация выглядела так:

git diff

   LLM

commit message

После добавления контекста и обработки изменений схема становится шире:

selected / staged changes

        diff collection

   filtering and normalization

 grouping related changes

 primary change detection

      context collection

 ┌───────────┼────────────┐
 ↓           ↓            ↓
Jira      branch       repository
task       name          policy

                 Git history retrieval

                   prompt construction

                          LLM

              validation and formatting

                editable commit message

LLM остается центральным генеративным компонентом. Но качество сообщения определяется не только моделью.

Необходимо правильно выбрать изменения, удалить шум, восстановить намерение, найти подходящие примеры и проверить, что результат соответствует фактическому diff.

Завершение первой части

В первой части книги мы начали с самой незаметной AI-функции — продолжения кода в позиции курсора.

Fill-in-the-Middle позволил модели учитывать не только код перед курсором, но и его продолжение. Autocompletion превратил этот механизм в полноценную IDE-функцию. Затем вокруг модели начали появляться дополнительные компоненты: поиск контекста, BM25-ранжирование и информация от языковых инструментов.

Next Edit Suggestions сделали следующий шаг: модель начала предсказывать не продолжение текущей строки, а будущее изменение в другом месте. Inline Edit позволил разработчику явно описывать нужное преобразование инструкцией.

Commit Generation завершает этот цикл.

разработчик начинает писать код

      AI продолжает ввод

 AI предлагает следующее изменение

 AI редактирует код по инструкции

    разработчик проверяет результат

 AI описывает готовые изменения в Git

На протяжении всей части роль модели постепенно расширялась. Сначала она предсказывала несколько следующих токенов. Затем начала учитывать контекст репозитория, предлагать изменения и выполнять инструкции.

Но во всех рассмотренных функциях сохранялся один и тот же принцип:

Модель не работает изолированно. Полезная AI-функция появляется только тогда, когда вокруг нее построена система сбора контекста, ограничений, проверок и взаимодействия с разработчиком.

В code completion такой системой были позиция курсора, prefix, suffix, найденные фрагменты и информация от IDE.

В commit generation — diff, задача, название ветки, правила репозитория и история Git.

Сама LLM может написать одну строку. Основная инженерная работа заключается в том, чтобы эта строка действительно описывала сделанное изменение.

Источники

  1. On Automatically Generating Commit Messages via Summarization of Source Code Changes - Luis Fernando Cortés-Coy et al., 2014. Система ChangeScribe на основе структурного анализа кода и шаблонов.
  2. Automatically Generating Commit Messages from Diffs using Neural Machine Translation - Siyuan Jiang, Ameer Armaly, Collin McMillan, 2017. Постановка генерации commit message как нейронного машинного перевода из diff.
  3. Neural-Machine-Translation-Based Commit Message Generation: How Far Are We? - Zhongxin Liu et al., 2018. Анализ NMT-подхода и retrieval-бейзлайн NNGen.
  4. CommitBench: A Benchmark for Commit Message Generation - Maximilian Schall, Tamara Czinczoll, Gerard de Melo, 2024. Проблемы датасетов и benchmark для генерации commit messages.
  5. Using Large Language Models for Commit Message Generation: A Preliminary Study - Linghao Zhang et al., 2024. Сравнение LLM с предыдущими подходами.
  6. RAG-Enhanced Commit Message Generation - Linghao Zhang et al., 2024. Использование retrieval похожих изменений и сообщений.
  7. Conventional Commits 1.0.0 - Conventional Commits, 2020. Спецификация структурированного формата commit messages.
  8. AI in version control - JetBrains, 2026. Генерация сообщений по diff и настройка инструкций.
  9. Staging and committing changes - Visual Studio Code, 2026. Генерация сообщения на основе staged changes.