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

Питання на співбесіді з 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, трекер) - якщо метадані не мають жити разом з репозиторієм.

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

Захист гілки перетворює домовленості («не пушимо в 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 - однакові правила для десятків репозиторіїв.

Докладніше в документації: GitHub: про rulesets

Проблема «зелені окремо, червоні разом». PR A і PR B пройшли CI окремо, кожен відносно старого main. Їх злили один за одним - і main зламався: A перейменував метод, а B додав новий виклик старої назви. Конфлікту в Git немає, а код не працює.

Вимога «гілка має бути актуальною» це лікує, але в активному репозиторії перетворюється на гонку: кожне злиття робить усі інші PR застарілими, їх оновлюють, CI перезапускається, і хтось інший знову зливає першим.

Merge queue:

  1. PR схвалений і зелений - автор додає його в чергу замість натискання Merge;
  2. GitHub створює тимчасову гілку gh-readonly-queue/main/... з main + усіма PR, що стоять перед ним у черзі, + цим PR;
  3. на цій комбінації запускаються обов'язкові перевірки;
  4. перевірки пройшли - 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 блокує всіх.

Докладніше в документації: GitHub: merge queue

Проблема: велику фічу розбили на маленькі 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.

Докладніше в документації: GitHub: stacked pull requests

Незгоди - нормальна частина рев'ю. Проблема не в них, а в тому, що вони тягнуться днями в коментарях і псують стосунки.

Як розв'язувати незгоду:

  1. розділити факти й смак. Баг, вразливість, порушення домовленостей команди - аргумент. «Я б написав інакше» - ні, якщо рішення автора теж коректне;
  2. спиратися на спільні правила, а не на авторитет: стайлгайд, архітектурні рішення (ADR), домовленості команди. Якщо правила немає - це привід його створити, а не виграти суперечку;
  3. перейти в розмову: після двох-трьох раундів коментарів 10 хвилин дзвінка вирішують більше, ніж ще десять повідомлень. Підсумок - записати в PR;
  4. ескалація до техліда чи ширшого обговорення, якщо згоди немає, - це нормальний механізм, а не поразка;
  5. «не блокує, але»: рецензент може погодитися злити зараз і створити задачу на покращення - якщо проблема не критична.

Принцип рецензента: схвалювати зміну, яка покращує стан кодової бази, навіть якщо вона не ідеальна. Вимога досконалості зупиняє розробку.

Як рев'ю стає вузьким місцем:

  • 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 підходить і для пошуку регресії продуктивності, не лише помилки.

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

Серверні хуки виконуються на сервері, що приймає 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 хвилинним і ламає роботу всієї команди при збоях.

Докладніше в документації: githooks: pre-receive

Якщо повідомлення комітів дотримуються 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:

  1. після кожного злиття в main дія аналізує нові коміти;
  2. відкриває чи оновлює PR «chore(main): release 1.5.0» зі зміненим CHANGELOG.md і номером версії;
  3. команда зливає цей PR, коли хоче випустити реліз;
  4. дія створює тег 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.

Обмеження: змінлог з комітів - для розробників. Для користувачів продукту часто потрібні людські нотатки до релізу, і автоматичний текст - лише чернетка для них.

Докладніше в документації: release-please

У великому проєкті з десятками залежностей бот, що відкриває окремий 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).

Докладніше в документації: Renovate: параметри конфігурації

Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії