Питання на співбесіді з Git
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
Ідентифікатори всіх об'єктів Git - хеші SHA-1 (160 біт, 40 шістнадцяткових символів). У 2017 році атака SHAttered показала практичну колізію SHA-1 - два різні PDF-файли з однаковим хешем. Згодом атаки з вибраним префіксом стали ще дешевшими.
Чим це загрожує Git: зловмисник теоретично може створити два об'єкти з однаковим хешем - безпечний для рецензії і шкідливий - і підмінити один іншим. Уся модель цілісності Git тримається на тому, що хеш однозначно визначає вміст.
Що зроблено вже зараз:
- Git використовує SHA-1 з виявленням колізій (sha1collisiondetection) - об'єкти, побудовані відомими методами атаки, розпізнаються й відхиляються;
- підтримка SHA-256 у Git з версії 2.29 (ще позначена як експериментальна, хоча вже стабільна для використання):
git init --object-format=sha256
git rev-parse --show-object-format # sha1 або sha256
Хеші в такому репозиторії - 64 символи.
Чому перехід повільний:
- SHA-1 і SHA-256 репозиторії несумісні: не можна напряму зробити
fetchчиpushміж ними. План переходу передбачає режим сумісності з таблицею відповідності хешів, але він ще не завершений; - хостинги: підтримка SHA-256 у GitHub, GitLab та інших з'являється поступово - перед створенням такого репозиторію треба перевірити, чи його приймає ваш сервер, CI і інструменти;
- екосистема: інструменти, що вважають хеш 40-символьним рядком (регулярні вирази, колонки в базах, парсери логів), ламаються;
- конвертація наявного репозиторію змінює всі хеші - посилання на коміти в задачах, changelog, документації стають недійсними.
Практичні висновки:
- для звичайних проєктів SHA-1 з виявленням колізій зараз достатній - реальна загроза потребує атаки на конкретний репозиторій і значних ресурсів;
- не покладайтеся на довжину хешу у власних інструментах: використовуйте
git rev-parse --show-object-formatі не обрізайте хеші жорстко до 40 символів; - для перевірки походження важливіші підписи комітів і тегів - їх теж стосується проблема SHA-1, тому сучасні підписи (SSH, GPG) хешують вміст власними алгоритмами;
- нові репозиторії на SHA-256 - розумний вибір для внутрішніх систем, де ви контролюєте всі інструменти; для відкритих проєктів поки що краще дочекатися повної підтримки хостингів.
Коміт неможливо змінити: будь-яка зміна, навіть у повідомленні, дає новий хеш. Але інколи до вже опублікованого коміту треба «дописати» інформацію: результат CI, посилання на рецензію, час деплою, заміток для розслідування.
git notes зберігає примітки окремо від комітів:
git notes add -m "Deployed to production 2026-10-04 14:20" a1b2c3d
git notes append -m "Rolled back: memory leak" a1b2c3d
git log -1 a1b2c3d
# commit a1b2c3d...
# ...
# Notes:
# Deployed to production 2026-10-04 14:20
# Rolled back: memory leak
Як це влаштовано: примітки - звичайні об'єкти Git в окремій гілці refs/notes/commits. Ця гілка містить коміти з деревом, де ім'я файлу - хеш коміту, до якого примітка, а вміст - текст. Тобто це історія приміток з власними комітами, а сам коміт-ціль не змінюється.
Простори імен - різні види метаданих окремо:
git notes --ref=ci add -m "build #4812: passed" HEAD
git notes --ref=deploy add -m "prod 2026-10-04" HEAD
git log --notes=ci --notes=deploy
Головна незручність - примітки не передаються за замовчуванням. git push і git fetch працюють лише з гілками й тегами, тож refs/notes/* треба вказати явно:
git push origin 'refs/notes/*'
git fetch origin 'refs/notes/*:refs/notes/*'
Або додати refspec у конфігурацію remote. GitHub давно не показує примітки в інтерфейсі, тому вони корисні передусім для внутрішніх інструментів.
Ще деталі:
- rebase і amend створюють нові коміти - примітки старих не переносяться, якщо не налаштувати
notes.rewriteRef; - злиття приміток з різних джерел - окрема команда
git notes mergeзі стратегіямиunion,cat_sort_uniq; - примітки не входять у хеш і не захищені підписом коміту - не використовуйте їх для чогось, що має бути незмінним.
Де notes доречні:
- CI записує результат збирання чи покриття до коміту;
- інструмент деплою позначає, які коміти й коли потрапили в продакшен;
- проєкти, що приймають патчі поштою, додають посилання на обговорення;
- особисті нотатки при розслідуванні історії.
Альтернативи: trailer-рядки в повідомленні (Reviewed-by:, Refs: #142) - якщо інформація відома до коміту; теги - для позначення конкретних точок; зовнішні системи (CI, трекер) - якщо метадані не мають жити разом з репозиторієм.
Захист гілки перетворює домовленості («не пушимо в main», «зливаємо лише після рев'ю й зелених тестів») на правила, які платформа примусово виконує.
Типовий набір правил для main:
- заборонити прямий push - зміни лише через PR;
- обов'язкове схвалення (1-2 рецензенти) і рев'ю власників коду (CODEOWNERS);
- скидати схвалення при нових комітах (dismiss stale approvals) - інакше після апруву можна дописати що завгодно;
- обов'язкові перевірки статусу (required status checks): тести, Pint, PHPStan мають бути зеленими;
- гілка має бути актуальною перед злиттям - або merge queue замість цього;
- заборонити force push і видалення гілки;
- розв'язані обговорення перед злиттям, за потреби - підписані коміти і лінійна історія.
Branch protection rules проти rulesets:
| Branch protection | Rulesets | |
|---|---|---|
| скільки діє на гілку | одне правило | кілька наборів одночасно, правила агрегуються |
| конфлікт налаштувань | - | діє найсуворіший варіант |
| вимкнути тимчасово | лише видалити | статус Disabled без видалення |
| хто бачить | адміністратори | усі з доступом на читання |
| винятки | обмежено | явний список bypass (ролі, команди, GitHub Apps) |
| рівень організації | ні | так (на планах Team/Enterprise) |
Обидва механізми працюють разом: застосовуються всі правила, що підходять.
Пастки:
- назва обов'язкової перевірки має збігатися з назвою job у CI. Перейменували job - PR зависає в очікуванні перевірки, яка ніколи не прийде;
- перевірка, що запускається не завжди (через
pathsу workflow), лишається «очікуваною» - PR не можна злити. Рішення - job, що завжди звітує успіхом, якщо змін немає; - адміністратори за замовчуванням можуть обходити правила - варто вирішити свідомо;
- bypass для ботів (релізний бот) - лише конкретним GitHub Apps, не всім.
Правила як код: rulesets можна експортувати й імпортувати в JSON чи керувати ними через API й Terraform - однакові правила для десятків репозиторіїв.
Проблема «зелені окремо, червоні разом». PR A і PR B пройшли CI окремо, кожен відносно старого main. Їх злили один за одним - і main зламався: A перейменував метод, а B додав новий виклик старої назви. Конфлікту в Git немає, а код не працює.
Вимога «гілка має бути актуальною» це лікує, але в активному репозиторії перетворюється на гонку: кожне злиття робить усі інші PR застарілими, їх оновлюють, CI перезапускається, і хтось інший знову зливає першим.
Merge queue:
- PR схвалений і зелений - автор додає його в чергу замість натискання Merge;
- GitHub створює тимчасову гілку
gh-readonly-queue/main/...зmain+ усіма PR, що стоять перед ним у черзі, + цим PR; - на цій комбінації запускаються обов'язкові перевірки;
- перевірки пройшли - PR вливається; впали - PR вилучається з черги, а черга за ним перебудовується без нього.
Черга може перевіряти кілька PR групою паралельно - це прискорює потік, коли PR багато.
Що треба налаштувати в CI:
on:
pull_request:
merge_group:
Подія merge_group - окрема від pull_request і push. Якщо її не додати, обов'язкові перевірки для черги не запустяться, і злиття впаде через відсутній статус. Сторонні CI мають реагувати на push у гілки з префіксом gh-readonly-queue/.
Що змінюється для команди:
- спосіб злиття визначає черга, а не автор;
mainзавжди зелений: те, що в нього потрапляє, перевірено саме в такій комбінації;- час до злиття зростає на тривалість CI - тому швидкий CI стає ще важливішим.
Коли не потрібна: невелика команда, кілька злиттів на день - достатньо вимоги актуальності гілки. Merge queue окупається там, де PR вливаються десятками на день і зламаний main блокує всіх.
Проблема: велику фічу розбили на маленькі PR, але кожен наступний залежить від попереднього. Чекати злиття першого, щоб почати другий, - повільно; складати все в один PR - втрачаємо переваги маленьких рев'ю.
Stacked PRs - ланцюжок залежних pull request-ів, де кожен спрямований не в main, а в гілку попереднього:
feat/ui → PR #3 (base: feat/api) ← верх
feat/api → PR #2 (base: feat/schema)
feat/schema → PR #1 (base: main) ← низ
main
Кожен PR показує лише свій шар змін: рецензент дивиться міграцію окремо від API і окремо від інтерфейсу. Принцип: якщо код залежить від іншого коду, залежність має бути в тій самій гілці чи нижче.
Головна складність - rebase. Після виправлення в нижній гілці всі верхні треба перебазувати. Вручну це втомливо, але Git уміє оновлювати весь ланцюжок одразу:
git switch feat/ui
git rebase --update-refs main
--update-refs під час rebase верхньої гілки пересуває й проміжні гілки (feat/schema, feat/api) на переписані коміти. Можна увімкнути за замовчуванням: git config rebase.updateRefs true.
Після злиття нижнього PR наступний треба переспрямувати на main і перебазувати. Якщо низ вливали через squash, у верхніх гілках лишаються «старі» коміти, яких у main вже немає за SHA, - тут допомагає git rebase --onto main feat/schema feat/api.
Інструменти: GitHub має нативну підтримку стеків (на момент написання - у публічному попередньому перегляді) з каскадним rebase на сервері і розширенням gh stack; є також сторонні інструменти на кшталт Graphite чи ghstack.
Коли варто: довга фіча, яку природно ділити на шари, і швидкий потік рев'ю. Коли ні: незалежні зміни - їм не потрібен стек, достатньо окремих PR від main; повільне рев'ю - стек з п'яти PR, що висять тиждень, перетворюється на постійний rebase.
Незгоди - нормальна частина рев'ю. Проблема не в них, а в тому, що вони тягнуться днями в коментарях і псують стосунки.
Як розв'язувати незгоду:
- розділити факти й смак. Баг, вразливість, порушення домовленостей команди - аргумент. «Я б написав інакше» - ні, якщо рішення автора теж коректне;
- спиратися на спільні правила, а не на авторитет: стайлгайд, архітектурні рішення (ADR), домовленості команди. Якщо правила немає - це привід його створити, а не виграти суперечку;
- перейти в розмову: після двох-трьох раундів коментарів 10 хвилин дзвінка вирішують більше, ніж ще десять повідомлень. Підсумок - записати в PR;
- ескалація до техліда чи ширшого обговорення, якщо згоди немає, - це нормальний механізм, а не поразка;
- «не блокує, але»: рецензент може погодитися злити зараз і створити задачу на покращення - якщо проблема не критична.
Принцип рецензента: схвалювати зміну, яка покращує стан кодової бази, навіть якщо вона не ідеальна. Вимога досконалості зупиняє розробку.
Як рев'ю стає вузьким місцем:
- PR чекають рев'ю по кілька днів, автори перемикаються між задачами й втрачають контекст;
- рев'ю робить одна людина;
- великі PR, які ніхто не хоче брати.
Що допомагає:
- домовленість про швидкість відгуку: перша реакція протягом робочого дня; не обов'язково повне рев'ю, але хоча б «подивлюсь після обіду»;
- рев'ю - пріоритетна робота, а не те, що роблять «коли буде час»: незлитий PR - незавершена робота;
- розподіл рев'юерів - автоматичне призначення команді за CODEOWNERS чи round-robin, щоб навантаження не падало на одних і тих самих;
- маленькі PR - їх рев'юють швидко;
- автоматика знімає з людей стиль і очевидні помилки;
- вимірювати час до першого відгуку й до злиття - без цифр проблема помітна лише відчуттям;
- парне програмування для складних змін - рев'ю відбувається під час написання.
Докладніше в документації: Google: як працювати з запереченнями в рев'ю
Ручний git bisect вимагає на кожному кроці перевірити код і сказати good чи bad. git bisect run робить це сам: на кожному кроці запускає скрипт і читає його код завершення.
git bisect start HEAD v2.3.0 # погана ревізія, потім добра
git bisect run php artisan test --compact --filter=VacancyExportTest
git bisect reset
На 1000 комітах це близько 10 запусків тесту замість ручного пошуку.
Як Git трактує код завершення:
| Код | Значення |
|---|---|
0 |
коміт добрий |
1-127, крім 125 |
коміт поганий |
125 |
коміт неможливо перевірити - пропустити (skip) |
інше (наприклад, 128 і більше) |
перервати bisect |
Скрипт для реальних умов - стара версія коду може потребувати іншої підготовки:
#!/bin/sh
# bisect.sh
composer install --no-interaction --quiet || exit 125 # не збирається - пропустити
php artisan migrate:fresh --env=testing --force --quiet || exit 125
php artisan test --compact --filter=VacancyExportTest
exit 125- коміт, на якому проєкт не збирається з не пов'язаних причин, не вважається «поганим» і не збиває пошук;- база - тестова (
--env=testing), щоб не зачепити робочу.
Тест, якого ще не було в історії: часто баг виявили сьогодні, а тест написали теж сьогодні - у старих комітах його немає. Рішення - тримати тест поза робочим деревом і копіювати його на кожному кроці:
cp /tmp/RegressionTest.php tests/Feature/RegressionTest.php
php artisan test --compact tests/Feature/RegressionTest.php
status=$?
rm tests/Feature/RegressionTest.php
exit $status
Корисні деталі:
git bisect start --first-parent- іти лише по комітах злиття основної гілки: знайти, який PR зламав, без заходу в проміжні коміти гілок;git bisect logіgit bisect replay- зберегти й повторити сесію;- терміни
good/badможна замінити (--term-old=fast --term-new=slow) - bisect підходить і для пошуку регресії продуктивності, не лише помилки.
Серверні хуки виконуються на сервері, що приймає push, і можуть відхилити його. На відміну від клієнтських, їх не можна пропустити через --no-verify - розробник їх не контролює.
Основні серверні хуки:
| Хук | Коли | Що може |
|---|---|---|
pre-receive |
один раз на весь push, до оновлення посилань | відхилити весь push |
update |
для кожного оновлюваного посилання окремо | відхилити окрему гілку чи тег |
post-receive |
після успішного оновлення | сповіщення, запуск деплою; відхилити вже не може |
pre-receive отримує на стандартний ввід рядки <старий SHA> <новий SHA> <посилання>:
#!/bin/sh
zero=0000000000000000000000000000000000000000
while read old new ref; do
# заборонити force push у main: старий коміт має бути предком нового
if [ "$ref" = "refs/heads/main" ] && [ "$old" != "$zero" ]; then
if ! git merge-base --is-ancestor "$old" "$new"; then
echo "Force push у main заборонено"
exit 1
fi
fi
done
Вивід хука повертається клієнту з префіксом remote:.
Де це доступно:
- власний сервер Git, GitLab self-managed, GitHub Enterprise Server - так;
- github.com - власні серверні хуки встановити не можна. Їхню роль виконують rulesets (заборона force push, обов'язкові підписи, обмеження на шляхи й розмір файлів) і push protection для секретів.
Серверні хуки проти CI:
| Серверний хук | CI | |
|---|---|---|
| коли | синхронно, під час push | асинхронно, після push |
| результат | push відхилено, коду на сервері немає | код уже на сервері, PR позначено червоним |
| час | секунди - інакше push «зависає» | хвилини, без обмежень |
| що перевіряти | політики: формат, заборонені файли, права, підписи | збирання, тести, аналіз |
Правило: у хуки - швидкі й безумовні політики, порушення яких не має з'явитися на сервері навіть у гілці (секрети, величезні бінарні файли, заборонені операції). Усе, що довге чи потребує середовища, - в CI. Хук, що запускає тести, робить кожен push хвилинним і ламає роботу всієї команди при збоях.
Якщо повідомлення комітів дотримуються Conventional Commits, з історії можна автоматично визначити, яким має бути наступний номер версії і що писати в змінлог.
Правила семантичного версіонування з комітів:
| Коміти з останнього релізу | Нова версія |
|---|---|
лише fix: |
патч: 1.4.2 → 1.4.3 |
є feat: |
мінорна: 1.4.2 → 1.5.0 |
feat!: чи футер BREAKING CHANGE: |
мажорна: 1.4.2 → 2.0.0 |
chore:, docs:, test: |
релізу не потребують |
release-please (від Google) працює через релізний pull request:
- після кожного злиття в
mainдія аналізує нові коміти; - відкриває чи оновлює PR «chore(main): release 1.5.0» зі зміненим
CHANGELOG.mdі номером версії; - команда зливає цей PR, коли хоче випустити реліз;
- дія створює тег
v1.5.0і GitHub Release з нотатками.
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
with:
release-type: php
semantic-release - альтернатива без проміжного PR: реліз публікується одразу після злиття. Більше автоматизації, менше контролю над моментом релізу.
Що потрібно для роботи:
- дисципліна повідомлень - перевірка в
commit-msgі в CI; при злитті через squash - перевірка заголовка PR, бо він стає повідомленням коміту; - осмислені коміти:
fix: typoв історіїmainпотрапить у змінлог; - права боту на запис і створення PR; якщо релізний PR має запускати CI, потрібен токен GitHub App чи PAT - події від стандартного
GITHUB_TOKENне запускають інші workflow.
Обмеження: змінлог з комітів - для розробників. Для користувачів продукту часто потрібні людські нотатки до релізу, і автоматичний текст - лише чернетка для них.
У великому проєкті з десятками залежностей бот, що відкриває окремий PR на кожне оновлення, швидко стає шумом, який команда ігнорує. Мета - щоб рутинні оновлення проходили без людей, а увагу отримували лише ризиковані.
Renovate - гнучкіша альтернатива Dependabot: підтримує більше екосистем, складні правила, групування, автозлиття й «панель залежностей» (Dependency Dashboard) - задачу зі списком усіх очікуваних оновлень.
{
"extends": ["config:recommended"],
"schedule": ["before 6am on monday"],
"minimumReleaseAge": "3 days",
"lockFileMaintenance": { "enabled": true },
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"matchPackageNames": ["laravel/**", "livewire/**", "filament/**"],
"groupName": "Laravel ecosystem"
},
{
"matchDepTypes": ["require-dev"],
"groupName": "dev dependencies",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
}
]
}
Що тут відбувається:
- автозлиття патчів і мінорних версій стабільних пакетів - якщо CI зелений, PR вливається сам;
- групи - пов'язані пакети оновлюються разом (екосистема Laravel), а не десятком окремих PR;
- мажорні версії - лише після схвалення на панелі, бо вони потребують міграції коду;
minimumReleaseAge- не брати версію, опубліковану щойно: зламані й скомпрометовані релізи зазвичай відкликають за перші дні;lockFileMaintenance- періодично оновлює транзитивні залежності вcomposer.lock, які інакше застарівають непомітно;- розклад - PR з'являються в певний час, а не протягом усього тижня.
Передумови для автозлиття:
- тести, яким можна довіряти - інакше автозлиття постачає баги в
main; - обов'язкові перевірки в захисті гілки - бот не повинен мати змоги злити червоний PR;
- моніторинг після деплою - швидко помітити регресію й відкотити.
Dependabot чи Renovate: Dependabot вбудований у GitHub і простіший у налаштуванні (групи, cooldown, ігнорування є й у ньому); Renovate виграє на складних правилах, монорепозиторіях і поза GitHub (GitLab, Bitbucket).
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії