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