Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Історія й гілки»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

git rebase -i відкриває список комітів гілки, де кожному можна задати дію. Так історію з «wip», «fix typo», «ще фікс» перетворюють на кілька змістовних комітів.

git rebase -i main
pick a1b2c3d Add invoice model
squash d4e5f6a fix typo
pick 9f8e7d6 Send invoice email
fixup 1a2b3c4 forgot import
reword 5d6e7f8 wip

Основні дії:

  • pick - лишити коміт як є.
  • reword - змінити повідомлення.
  • squash - злити з попереднім, об'єднавши повідомлення.
  • fixup - злити з попереднім, відкинувши повідомлення.
  • edit - зупинитися на коміті, щоб змінити його чи розбити на кілька.
  • drop або видалити рядок - прибрати коміт.
  • Переставити рядки - змінити порядок комітів.

Зручний прийом - fixup-коміти:

git commit --fixup=a1b2c3d     # «виправлення до коміту a1b2c3d»
git rebase -i --autosquash main # сам поставить fixup у потрібне місце

Навіщо: рев'юеру легше читати кілька логічних комітів, ніж двадцять випадкових; git bisect і git blame працюють точніше; коміт можна відкотити цілком.

Обережно: це переписування історії. Лише для своєї гілки, а потім - git push --force-with-lease. Якщо щось пішло не так, git rebase --abort повертає все як було, а після завершення врятує git reflog.

Багато команд обходяться без цього, вливаючи PR через squash merge - тоді вся гілка стає одним комітом у main.

Докладніше в документації: git rebase: interactive mode

git bisect шукає коміт, що вніс помилку, двійковим пошуком. Ви називаєте один «поганий» коміт (де баг є) і один «хороший» (де його ще не було), а Git щоразу переходить на середину проміжку й питає, чи є там баг.

git bisect start
git bisect bad                 # поточний коміт - зламаний
git bisect good v2.3.0         # у цьому релізі все працювало

# Git перемикається на коміт посередині
# перевіряєте й кажете:
git bisect good    # або
git bisect bad

# ...через кілька кроків:
# a1b2c3d is the first bad commit

git bisect reset               # повернутися, звідки почали

Серед 1000 комітів винуватця знайдено за ~10 перевірок.

Автоматично - якщо є команда, що відповідає «добре/погано» кодом виходу:

git bisect run php artisan test --filter=InvoiceTotalTest

Код 0 - коміт хороший, 1-127 (крім 125) - поганий, 125 - пропустити (наприклад, коміт не збирається).

Що допомагає bisect:

  • Невеликі коміти, кожен з яких працює. Якщо коміт «напівготовий» і падає з іншої причини, bisect заплутається - такі коміти пропускають (git bisect skip).
  • Тест, що відтворює баг, - його пишуть першим, а потім ганяють через bisect run.

Знайшовши коміт, видно не лише рядок, а й контекст: повідомлення, задачу, автора - часто це пояснює, чому баг з'явився.

Докладніше в документації: git bisect

Звичайний git rebase main переносить усі коміти поточної гілки, яких немає в main. Але інколи потрібно перенести лише частину комітів чи змінити основу на зовсім іншу гілку. Для цього є --onto:

git rebase --onto <нова-основа> <стара-основа> <гілка>

Git бере коміти з діапазону стара-основа..гілка і відтворює їх поверх нова-основа.

Сценарій 1 - гілку створено не від тієї гілки. Гілку feature почали від develop, а треба було від main:

main ── A
         \
develop   B ── C
                \
feature          D ── E
git rebase --onto main develop feature
main ── A ── D' ── E'    (feature)

Коміти B і C з develop у feature не потрапили.

Сценарій 2 - залежні pull request. Гілка part-2 побудована поверх part-1. Після злиття part-1 через squash її коміти в main мають інші хеші, і звичайний git rebase main намагатиметься застосувати їх повторно з конфліктами:

git rebase --onto main part-1 part-2

Переноситься лише власна робота part-2.

Сценарій 3 - видалити коміти з середини гілки:

git rebase --onto HEAD~5 HEAD~3

Коміти HEAD~4 і HEAD~3 зникають, решта переноситься на HEAD~5.

Що пам'ятати:

  • як і будь-який rebase, --onto створює нові коміти з новими хешами - для вже опублікованої спільної гілки це переписування історії;
  • якщо заплуталися - git rebase --abort під час процесу чи git reflog після нього;
  • --update-refs (Git 2.38+) оновлює й проміжні гілки в ланцюжку - зручно для стопки залежних гілок.

Докладніше в документації: git rebase: перенесення гілки з --onto

Злити гілку feature у main можна трьома способами, і від вибору залежить вигляд історії.

Fast-forward - можливий, коли main не просунувся після створення feature. Git просто пересуває вказівник main на останній коміт feature, нового коміту не створюється:

до:     main ── A                після:  A ── B ── C   (main, feature)
                 \
                  B ── C (feature)

Історія лінійна, але факт існування гілки зникає: не видно, які коміти були однією задачею.

--no-ff - завжди створює коміт злиття, навіть коли fast-forward можливий:

git merge --no-ff feature
A ────────── M   (main)
 \          /
  B ── C ──      (feature)

Видно межі задачі; відкотити всю функціональність можна одним git revert -m 1 M. Ціна - більше комітів злиття в історії.

Squash - усі зміни гілки стають одним новим комітом в main:

git merge --squash feature
git commit -m "Add invoice export"

Історія main чиста: одна задача - один коміт. Але:

  • проміжні коміти гілки в main не потрапляють - деталізація для git bisect і blame втрачається;
  • Git не вважає feature злитою (git branch --merged її не покаже), а git branch -d feature відмовиться її видаляти;
  • якщо продовжити роботу в тій самій гілці, наступний squash може дати конфлікти з уже злитими змінами.

Порівняння:

Fast-forward --no-ff Squash
коміт злиття ні так ні
проміжні коміти в main так так ні
межі задачі видно ні так так (один коміт)

Налаштування: git config merge.ff only дозволяє лише fast-forward (злиття впаде, якщо воно неможливе), pull.ff only - те саме для git pull. На GitHub і GitLab спосіб злиття pull request обирається в налаштуваннях репозиторію.

Докладніше в документації: git merge: fast-forward

git blame показує для кожного рядка файлу, в якому коміті й ким він востаннє змінений:

git blame app/Services/Billing.php
git blame -L 40,60 app/Services/Billing.php    # лише рядки 40-60
a1b2c3d4 (Olena 2026-03-14 41) $total = round($subtotal * 1.2, 2);

Мета - не «кого звинуватити», а «чому так»: знайдений хеш веде до коміту з повідомленням, pull request і обговоренням, де видно причину рішення.

git show a1b2c3d4

Проблема: шумні коміти. Масове форматування (Pint, Prettier), перейменування чи перенесення коду роблять автором кожного рядка «коміт форматування», і справжня історія ховається.

Що допомагає:

Параметр Що робить
-w ігнорує зміни пробілів
-M розпізнає рядки, переміщені в межах файлу
-C розпізнає рядки, перенесені з інших файлів того самого коміту (-C -C -C - з будь-якого коміту)
--ignore-rev <хеш> пропустити конкретний коміт і показати попереднього автора

Файл ігнорованих комітів - рішення для всієї команди:

# .git-blame-ignore-revs
# Pint: форматування всього проєкту
3f9e1a2b7c...
git config blame.ignoreRevsFile .git-blame-ignore-revs

GitHub автоматично враховує файл .git-blame-ignore-revs у корені репозиторію у своєму інтерфейсі blame.

Коли рядок змінювався багато разів:

  • git log -L 40,60:app/Services/Billing.php - вся історія змін саме цього фрагмента, з diff кожного кроку;
  • git log -S "1.2" - коміти, що додали чи прибрали конкретний вираз;
  • в IDE blame вбудований («Annotate with Git Blame» у PhpStorm) з можливістю переходити до попередньої ревізії рядка.

Докладніше в документації: git blame