JavaRush /Курси /Claude code /Дисципліна комітів і упаковка PR

Дисципліна комітів і упаковка PR

Claude code
Рівень 18 , Лекція 3
Відкрита

1. Коміт — не збереження, а точка відкату

На цьому етапі баг уже не висить у повітрі: першопричину зафіксовано, регресійний тест упіймав стару поведінку, мінімальне виправлення дало невеликий diff і докази закриття дефекту. Тепер задача змінюється — лишилося упакувати зміну так, щоб її можна було читати, відкотити й рев’юити без археології.

Коли ви тільки починаєте працювати з Git, коміт легко переплутати зі звичайним збереженням файла: раз код уже написаний і лежить на диску, здається, що коміт — це просто ще один спосіб сказати комп’ютеру «запам’ятай, будь ласка». Але в реальній розробці він потрібен не комп’ютеру, а вам завтрашньому, рев’юеру й команді як малий, завершений і зрозумілий крок.

Корисно тримати в голові дуже просту різницю:

Що це Для кого На яке запитання відповідає
Збереження файла Для вас просто зараз «Я не втрачу текст?»
Коміт Для вас і команди «Який завершений крок я зробив?»
PR Для рев’юера та історії проєкту «Що змінилося, навіщо і як це перевірити?»

У нашому проєкті AI Commerce Growth OS візьмемо знайомий приклад: ви виправляєте баг, де checkout з порожнім кошиком повертав 500 замість зрозумілої 400. Якщо все це лежить одним великим брудним diff’ом, відкотити страшно, а читати важко; якщо ж у вас є маленькі коміти, ви буквально ріжете задачу на перевірювані шматки.

Тут корисно ще раз пригадати ідею з попередніх лекцій: Claude edits = пропозиції, виражені як diff’и. Коміт — момент, коли ви кажете: «Цей шматок я зрозумів і готовий за нього відповідати». Поки його немає, зміна висить у сірій зоні між «схоже, працює» та «я відповідаю».

Зазвичай задача живе в окремій гілці. Гілка відповідає на запитання «над чим ми взагалі працюємо?», а коміт — на «який крок завершено?». Наприклад:

git checkout -b bugfix/issue-432-empty-cart-checkout
# Перемкнулися на нову гілку 'bugfix/issue-432-empty-cart-checkout'

Це вже непогано: з імені видно, що йдеться про багфікс і конкретну задачу. Але якщо всередині один гігантський коміт правки, користі від акуратної назви майже нуль. Гілка без гарних комітів — як папка, де все названо нове2_фінал_точно.

Гарний коміт — це маленька точка відкату з понятним підписом, а не склад усього, що випадково накопичилося за вечір.

2. Одну задачу — на маленькі коміти

Найчастіша проблема у новачків тут не в Git-командах, а в масштабі мислення. Дуже хочеться спочатку довести все до кінця, а потім одним комітом зафіксувати весь результат: баг же один, отже і коміт має бути один. Але рев’юер читає не назву задачі з трекера, а реальну історію змін — скиньте її в один мішок, і вона перестає бути історією.

Найкращий орієнтир тут — схвалений план реалізації, який ви підготували раніше: кожен крок плану — кандидат на окремий коміт. Для бага про порожній кошик нарізка природна: спочатку регресійний тест, який доводить дефект, потім мінімальне виправлення в контролері, щоб повернути 400 до виклику сервісу. Історія виходить здоровою:

git log --oneline -2
# 7f3c1a2 fix(orders): повернути 400 під час checkout з порожнім кошиком
# 19ab8d4 test(orders): додати регресійний тест для порожнього кошика

Подивіться, скільки зрозумілості в двох рядках: навіть без відкриття файлів видно, що спочатку ми зафіксували дефект тестом, а потім виправили поведінку. Це не просто красиво — це зручно для rollback, для blame, для вашого майбутнього «я», яке через три тижні зрозуміє, звідки в checkout окрема перевірка.

Практично це означає, що ви не робите сліпий git add ., якщо Claude дорогою зачепив ще щось. Додаєте лише файли поточного кроку:

git add src/orders/OrderControllerTest.java
git commit -m "test(orders): додати регресійний тест для порожнього кошика"

git add src/orders/OrderController.java
git commit -m "fix(orders): повернути 400 під час checkout з порожнім кошиком"

Такий підхід особливо важливий у роботі з AI. Claude любить покласти поруч із корисною зміною ще й дрібний «бонус»: форматування в сусідньому файлі, перейменування змінної трохи далі по коду, імпорт «заодно». На рівні однієї сесії це здається нешкідливим, але на рівні коміту — уже шум. Файл не повинен потрапляти в коміт тільки тому, що «вже змінювався».

Є гарний простий тест: якщо ви не можете одним реченням пояснити, що саме зробив цей коміт, значить коміт занадто великий.

Саме так дисципліна комітів стримує розповзання scope на рівні PR: поки задача живе в маленьких тематичних комітах, вам набагато важче непомітно протягнути в багфікс ще рефакторинг, оновлення залежності й «прибирання по дорозі». Git у цьому сенсі — хороший вихователь: мовчазний, але принциповий.

3. Повідомлення коміту проти археології

Тепер до найбільш недооціненого місця всієї історії — тексту commit message. Багато хто ставиться до нього як до формальності, тим більше що Claude або IDE «щось згенерують». Але це не підпис заради підпису, а коротка документація до одного інженерного кроку: якщо вона слабка, історія проєкту швидко перетворюється на археологічний розкоп.

Гарне повідомлення коміту відповідає щонайменше на два запитання: що змінилося і в якому контексті це потрібно. Формат може відрізнятися від команди до команди, але сенс лишається — і часто домовленість зручно тримати прямо в CLAUDE.md, щоб Claude не вигадував стиль заново в кожній задачі.

Наприклад, для Commerce OS правило може виглядати так:

## Правила комітів

Формат: <тип>(<область>): <коротка дія>

Приклади:
- fix(orders): повернути 400 під час checkout з порожнім кошиком
- test(orders): додати регресійний тест для порожнього кошика
- docs(api): уточнити приклад відповіді для order history

Зверніть увагу: тут немає магії. Мова — російська чи англійська — справа домовленості команди. Важливіше інше: однакова форма робить історію сканованою очима.

Порівняйте:

Слабкий варіант Сильний варіант
fix
fix(orders): повернути 400 під час checkout з порожнім кошиком
правки
test(orders): додати регресійний тест для порожнього кошика
final fix
fix(orders): перервати checkout до виклику CheckoutService

Різниця тут не косметична: за слабким варіантом через місяць не зрозуміти, що зроблено, за сильним — зрозуміло навіть без відкриття diff.

Іноді корисно додати й тіло коміту, коли причина неочевидна:

git commit -m "fix(orders): повернути 400 під час checkout з порожнім кошиком" \
  -m "Порожній список товарів доходив до CheckoutService і закінчувався серверною помилкою.
Перевірку додано в OrderController до виклику сервісу. Пов’язано з ISSUE-432."

Таке тіло особливо цінне, коли ви не просто змінюєте рядок, а фіксуєте мотивацію рішення: чому перевірка в контролері, а не в сервісі? Чому 400, а не 422? Коротке пояснення економить час тому, хто потім читатиме історію.

Claude тут справді може допомогти — але лише коли ви даєте йому правильну задачу:

Подивись на поточний diff і запропонуй одне повідомлення коміту.
Не вигадуй змін, яких немає.
Якщо бачиш файли поза поточним кроком плану, спочатку переліч їх окремо.
Поверни короткий заголовок і, за потреби, 1-2 рядки тіла коміту.

Ключова думка тут — Claude пропонує, ви звіряєте з diff. Якщо повідомлення вийшло красивим, але в ньому згадано тест, який ви не додавали, або файл не з коміту, — брати такий текст без перевірки не можна: упевнений тон не робить commit message правдивим.

4. Від історії комітів до PR за пів хвилини

Коли маленькі коміти вже зібрані, легко подумати: «Ну все, тепер рев’юер сам розбереться по історії». Іноді — так, але частіше — ні. PR — це не просто пачка комітів, а упакована розповідь про зміну. Рев’юер майже ніколи не починає з git log: він починає із заголовка, опису й секції перевірки — і якщо там хаос, то далі diff читається вже в роздратованому режимі.

Уявіть людину, яка відкриває ваш PR між двома зустрічами, з горнятком кави й десятьма сповіщеннями. Вона не хоче бути археологом — вона хоче дуже швидко зрозуміти суть, перевірку й межі.

Схема тут проста:

flowchart TD
    A[Схвалений план] --> B[Маленькі commit'и]
    B --> C[Читабельна історія Git]
    C --> D[PR_DESCRIPTION.md]
    D --> E[PR, готовий до рев’ю]

І ще один корисний погляд на артефакти:

Артефакт На яке запитання відповідає
Гілка Над якою задачею ми працюємо?
Коміт Який крок завершено?
Повідомлення коміту Що саме і навіщо змінилося в цьому кроці?
PR title У чому суть усієї зміни цілком?
PR_DESCRIPTION.md
Як перевірити результат, які ризики й що не входило в 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 не переказує кожен рядок — він відповідає на чотири запитання: що змінилося, чому, як перевірити і що залишилося за межами задачі. І ось тут дуже доречні результати попередніх лекцій: формулювання першопричини йде в «Навіщо», regression evidence перетворюється на «Як перевірити», approved plan допомагає чесно заповнити «Поза scope».

Ось як може виглядати PR_DESCRIPTION.md для нашого bugfix у Commerce OS:

# Огляд PR — ISSUE-432

## Що змінилося
- `OrderController#checkout`: додано перевірку порожнього кошика
- `OrderControllerTest`: додано регресійний тест `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 — помічник, не редактор

Коли справа доходить до оформлення комітів і 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 і нагадування перед комітом, Claude працює помітно краще, тому що в нього з’являється контур. Але навіть за ідеального контуру фінальна інженерна відповідальність лишається на вас — і це не бюрократія, а проста логіка: merge робить команда, баги ловить production, а не красиво написаний markdown.

У добре упакованому PR рев’юер не повинен гадати, що сталося. Він відкриває зміну й одразу бачить історію: який баг виправлено, чому обрано саме це рішення, чим перевірено, де межі задачі. Коли ви вмієте збирати таку історію з маленьких комітів і чесного PR_DESCRIPTION.md, ваш diff перестає бути просто набором правок і стає нормальним інженерним артефактом.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ