1. Коммит — не сохранение, а точка отката
На этом этапе баг уже не висит в воздухе: root cause зафиксирован, regression test поймал старое поведение, минимальный fix дал небольшой diff и доказательства закрытия дефекта. Теперь задача меняется — осталось упаковать изменение так, чтобы его можно было читать, откатывать и ревьюить без археологии.
Когда вы только начинаете работать с Git, commit легко перепутать с обычным сохранением файла: раз код уже написан и лежит на диске, кажется, что commit — это просто ещё один способ сказать компьютеру «запомни, пожалуйста». Но в реальной разработке нужен он не компьютеру, а вам завтрашнему, ревьюеру и команде как маленький, законченный и объяснимый шаг.
Полезно держать в голове очень простую разницу:
| Что это | Для кого | На какой вопрос отвечает |
|---|---|---|
| Сохранение файла | Для вас прямо сейчас | «Я не потеряю текст?» |
| Commit | Для вас и команды | «Какой законченный шаг я сделал?» |
| PR | Для ревьюера и истории проекта | «Что изменилось, зачем и как это проверить?» |
В нашем проекте AI Commerce Growth OS возьмём знакомый пример: вы чините баг, где checkout с пустой корзиной возвращал 500 вместо понятной 400. Если всё это лежит одним большим грязным diff’ом, откатывать страшно, а читать тяжело; если же у вас есть маленькие commit’ы, вы буквально режете задачу на проверяемые куски.
Здесь полезно ещё раз вспомнить идею из предыдущих лекций: Claude edits = proposals expressed as diffs. Commit — момент, когда вы говорите: «Этот кусок я понял и готов за него отвечать». Пока его нет, изменение висит в серой зоне между «похоже, работает» и «я отвечаю».
Обычно задача живёт в отдельной ветке. Ветка отвечает на вопрос «над чем мы вообще работаем?», а commit — на «какой шаг завершён?». Например:
git checkout -b bugfix/issue-432-empty-cart-checkout
# Switched to a new branch 'bugfix/issue-432-empty-cart-checkout'
Это уже неплохо: из имени видно, что речь о багфиксе и конкретной задаче. Но если внутри один гигантский commit правки, пользы от аккуратного имени почти ноль. Ветка без хороших commit’ов — как папка, где всё называется новое2_финал_точно.
Хороший commit — это маленькая точка отката с понятной подписью, а не склад всего, что случайно накопилось за вечер.
2. Одну задачу — на маленькие коммиты
Самая частая проблема у новичков здесь не в Git-командах, а в масштабе мышления. Очень хочется сначала довести всё до конца, а потом одним commit’ом зафиксировать весь результат: баг ведь один, значит и commit должен быть один. Но ревьюер читает не название задачи из трекера, а реальную историю изменений — свалите её в один мешок, и она перестаёт быть историей.
Лучший ориентир здесь — approved implementation plan, который вы подготовили раньше: каждый шаг плана — кандидат в отдельный commit. Для бага про пустую корзину нарезка естественная: сначала regression-тест, который доказывает дефект, потом минимальное исправление в контроллере, чтобы вернуть 400 до вызова сервиса. История получается здоровая:
git log --oneline -2
# 7f3c1a2 fix(orders): вернуть 400 при checkout с пустой корзиной
# 19ab8d4 test(orders): добавить regression-тест для пустой корзины
Посмотрите, сколько понятности в двух строках: даже без открытия файлов видно, что сначала мы зафиксировали дефект тестом, а потом исправили поведение. Это не просто красиво — это удобно для rollback, для blame, для будущего вас, который через три недели поймёт, откуда в checkout отдельная проверка.
Практически это означает, что вы не делаете слепой git add ., если Claude по пути затронул что-то ещё. Добавляете только файлы текущего шага:
git add src/orders/OrderControllerTest.java
git commit -m "test(orders): добавить regression-тест для пустой корзины"
git add src/orders/OrderController.java
git commit -m "fix(orders): вернуть 400 при checkout с пустой корзиной"
Такой подход особенно важен в работе с AI. Claude любит положить рядом с полезным изменением ещё и мелкий «бонус»: форматирование в соседнем файле, переименование переменной чуть дальше по коду, импорт «заодно». На уровне одной сессии это кажется безобидным, но на уровне commit’а — уже шум. Файл не должен попадать в commit только потому, что «уже менялся».
Есть хороший простой тест: если вы не можете в одном предложении объяснить, что именно сделал этот commit, значит commit слишком большой.
Именно так commit discipline держит scope creep на уровне PR: пока задача живёт в маленьких тематических commit’ах, вам гораздо труднее незаметно протащить в багфикс ещё рефакторинг, обновление зависимости и «уборку по пути». Git в этом смысле — хороший воспитатель: молчаливый, но принципиальный.
3. Сообщение коммита против археологии
Теперь к самому недооценённому месту всей истории — тексту commit message. Многие относятся к нему как к формальности, тем более что Claude или IDE «что-нибудь сгенерируют». Но это не подпись ради подписи, а краткая документация к одному инженерному шагу: если она слабая, история проекта быстро превращается в археологический раскоп.
Хорошее сообщение коммита отвечает минимум на два вопроса: что изменилось и в каком контексте это нужно. Формат может отличаться от команды к команде, но смысл остаётся — и часто соглашение удобно держать прямо в CLAUDE.md, чтобы Claude не изобретал стиль заново в каждой задаче.
Например, для Commerce OS правило может выглядеть так:
## Правила коммитов
Формат: <тип>(<область>): <краткое действие>
Примеры:
- fix(orders): вернуть 400 при checkout с пустой корзиной
- test(orders): добавить regression-тест для пустой корзины
- docs(api): уточнить пример ответа для order history
Заметьте: тут нет магии. Язык — русский или английский — дело договорённости команды. Важнее другое: одинаковая форма делает историю сканируемой глазами.
Сравните:
| Слабый вариант | Сильный вариант |
|---|---|
|
|
|
|
|
|
Разница здесь не косметическая: по слабому варианту через месяц не понять, что сделано, по сильному — понятно даже без открытия diff.
Иногда полезно добавить и тело commit’а, если причина неочевидна:
git commit -m "fix(orders): вернуть 400 при checkout с пустой корзиной" \
-m "Пустой список товаров доходил до CheckoutService и заканчивался серверной ошибкой.
Проверка добавлена в OrderController до вызова сервиса. Связано с ISSUE-432."
Такое тело особенно ценно, когда вы не просто меняете строчку, а фиксируете мотивацию решения: почему проверка в контроллере, а не в сервисе? Почему 400, а не 422? Короткое пояснение экономит время тому, кто потом будет читать историю.
Claude здесь действительно может помочь — но только если вы даёте ему правильную задачу:
Посмотри на текущий diff и предложи одно сообщение коммита.
Не придумывай изменений, которых нет.
Если видишь файлы вне текущего шага плана, сначала перечисли их отдельно.
Верни короткий заголовок и, если нужно, 1-2 строки тела коммита.
Ключевая мысль здесь — Claude предлагает, вы сверяете с diff. Если сообщение получилось красивым, но в нём упомянут тест, которого вы не добавляли, или файл не из commit’а, — брать такой текст без проверки нельзя: уверенный тон не делает commit message правдивым.
4. От истории коммитов к PR за полминуты
Когда маленькие commit’ы уже собраны, легко подумать: «Ну всё, теперь ревьюер сам разберётся по истории». Иногда — да, но чаще — нет. PR — это не просто пачка commit’ов, а упакованный рассказ об изменении. Ревьюер почти никогда не начинает с git log: он начинает с заголовка, описания и секции проверки — и если там хаос, то дальше diff читается уже в раздражённом режиме.
Представьте человека, который открывает ваш PR между двумя встречами, с кружкой кофе и десятью уведомлениями. Он не хочет быть археологом — он хочет очень быстро понять суть, проверку и границы.
Схема здесь простая:
flowchart TD
A[Approved plan] --> B[Маленькие commit'ы]
B --> C[Читаемая история Git]
C --> D[PR_DESCRIPTION.md]
D --> E[Review-ready PR]
И ещё один полезный взгляд на артефакты:
| Артефакт | На какой вопрос отвечает |
|---|---|
| Ветка | Над какой задачей мы работаем? |
| Commit | Какой шаг завершён? |
| Сообщение коммита | Что именно и зачем изменилось в этом шаге? |
| PR title | В чём суть всего изменения целиком? |
|
Как проверить результат, какие риски и что не входило в scope? |
Очень часто начинающие разработчики делают PR title слишком техническим: add null check in OrderController. Формально это правда, но ревьюеру полезнее видеть смысл на уровне поведения: Запретить checkout с пустой корзиной и вернуть 400. Первый описывает реализацию, второй — результат, и для PR важнее второй. То же самое касается description: не дубль diff, а сжатый контекст.
Если в команде есть шаблон PR, это сильно упрощает жизнь — даже простой шаблон уже держит структуру:
## Что изменилось
## Зачем
## Как проверить
## Риски и крайние случаи
## Вне scope
Это кажется мелочью, но на деле спасает от главной болезни AI-упаковки — красивого, но бесформенного текста: когда у вас есть фиксированные секции, и вы, и Claude гораздо реже скатываетесь в расплывчатое «улучшили обработку ошибок».
5. PR_DESCRIPTION.md: walkthrough и границы
Теперь самое главное — сам PR walkthrough. Это не технический термин ради солидности, а маршрут для ревьюера: вы не заставляете человека прыгать по файлам вслепую, а проводите его через изменение сверху вниз. Храните его как PR_DESCRIPTION.md, а потом переносите в тело pull request.
Хороший walkthrough не пересказывает каждую строку — он отвечает на четыре вопроса: что изменилось, почему, как проверить и что осталось за рамками задачи. И вот здесь очень кстати пригождаются результаты предыдущих лекций: Root cause statement идёт в «Зачем», regression evidence превращается в «Как проверить», approved plan помогает честно заполнить «Вне scope».
Вот как может выглядеть PR_DESCRIPTION.md для нашего bugfix в Commerce OS:
# PR walkthrough — ISSUE-432
## Что изменилось
- `OrderController#checkout`: добавлена проверка пустой корзины
- `OrderControllerTest`: добавлен regression-тест `rejectsEmptyCart`
## Зачем
Пустая корзина доходила до `CheckoutService` и заканчивалась серверной ошибкой,
вместо понятного ответа клиента `400`.
## Как проверить
- `./gradlew :orders:test --tests OrderControllerTest.rejectsEmptyCart`
- POST `/api/orders` с пустым списком товаров должен вернуть `400`
## Риски и крайние случаи
Контракт успешного checkout не менялся. Изменение затрагивает только сценарий пустой корзины.
## Вне scope
- логика deduplication в корзине
- endpoint `/orders/preview`
Обратите внимание, насколько это короче реального diff’а — и насколько полезнее на первом чтении. Ревьюер уже понимает, где искать изменения и как воспроизвести результат, ему не нужно гадать, почему вдруг появился новый тест.
Особенно важен раздел «Как проверить». Здесь абстракции вроде «все тесты зелёные» бесполезны — ревьюер не знает, что именно вы запускали; гораздо сильнее работает конкретика:
./gradlew :orders:test --tests OrderControllerTest.rejectsEmptyCart # тест проходит
curl -X POST http://localhost:8080/api/orders -d '{ "items": [] }' # HTTP 400
Даже если ревьюер не выполнит эти команды буквально, сам факт их наличия показывает: у задачи есть воспроизводимая цепочка проверки, а не просто вера в хороший исход.
Не менее полезен раздел «Вне scope» — он выглядит скучно, но именно он защищает PR от ложных ожиданий. Если вы чините checkout с пустой корзиной, это не значит, что вы обязаны заодно переписать preview endpoint, улучшить валидацию всех заказов и поправить старый TODO в соседнем сервисе. Когда вы честно называете границы, ревьюер видит их и меньше склонен считать PR «неполным».
А вот раздел с рисками нужен не для драматизации, а для честности. Иногда риск действительно минимальный — тогда так и пишите. Но если есть открытый вопрос — не проверялся старый мобильный клиент, не затрагивался нестандартный flow, — лучше назвать это явно. Честный PR всегда сильнее идеального по тону, но мутного по содержанию.
6. Claude в packaging — помощник, не редактор
Когда дело доходит до оформления commit’ов и PR, Claude очень удобен: сжимает diff в понятный текст, не даёт забыть секции в PR_DESCRIPTION.md, предлагает заголовок, напоминает про проверки. Но именно здесь легче всего расслабиться и начать принимать хороший текст за хорошую инженерную упаковку.
Безопасный способ работать с Claude в packaging-фазе выглядит так:
Посмотри на текущий diff и подготовь черновик `PR_DESCRIPTION.md`.
Опирайся только на:
- текущий diff;
- последние commit'ы;
- список реально выполненных проверок.
Не придумывай файлов, тестов и рисков, которых нет.
Если замечаешь изменения вне scope, сначала предупреди об этом.
Такой запрос хорош тем, что вы сразу ограничиваете источник правды: не «вспомни, что мы делали», не «суммируй сессию», а именно — вот diff, вот commit’ы, вот реальные проверки. Так меньше шанс, что Claude красиво додумает то, чего в PR нет.
После этого ваша работа — сверить черновик с фактами: открываете git diff, git log --oneline, смотрите свои команды проверки и буквально сравниваете. Лишний файл — удаляете. «Обновлены API docs», которых вы не трогали, — исправляете. «All related tests passed» при одной прицельной проверке — переписываете честно.
Если у команды уже настроен CLAUDE.md, шаблон PR и напоминание перед commit, Claude работает заметно лучше, потому что у него появляется контур. Но даже при идеальном контуре финальная инженерная ответственность остаётся у вас — и это не бюрократия, а простая логика: merge делает команда, баги ловит production, а не красиво написанный markdown.
В хорошо упакованном PR ревьюер не должен гадать, что произошло. Он открывает изменение и сразу видит историю: какой баг исправлен, почему выбрано это решение, чем проверено, где границы задачи. Когда вы умеете собирать такую историю из маленьких commit’ов и честного PR_DESCRIPTION.md, ваш diff перестаёт быть просто набором правок и становится нормальным инженерным артефактом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ