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

Git: питання на співбесіді рівня Junior

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

35 питань

Обидві команди переносять зміни з однієї гілки в іншу, але по-різному формують історію.

git merge main (у своїй гілці) створює коміт злиття з двома батьками. Історія показує, як було насправді: дві гілки розвивалися паралельно і зійшлися.

git rebase main переписує коміти гілки так, ніби вона почалася з останнього коміту main: кожен коміт застосовується заново й отримує новий хеш. Історія стає лінійною, без комітів злиття.

git switch feature
git rebase main          # перебудувати feature поверх свіжого main
git switch main
git merge --ff-only feature   # тепер злиття - просте перемотування

Що обирати:

  • Rebase - для власної гілки, щоб підтягнути свіжий main і отримати чисту історію перед pull request.
  • Merge - для злиття спільних гілок і коли важливо зберегти, як саме йшла робота.

Головне правило rebase: не переписувати коміти, які вже опубліковані й використовуються іншими. Після rebase гілку доведеться відправляти з --force-with-lease, а в колег, що працювали поверх старих комітів, історія розійдеться.

Конфлікти бувають і там, і там. Але при rebase їх розв'язують по коміту: якщо гілка з 10 комітів зачіпає те саме місце, конфлікт може з'явитися кілька разів.

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

Спосіб залежить від того, чи коміт уже відправлено в спільний репозиторій.

Коміт ще локальний - git reset пересуває гілку назад:

git reset --soft HEAD~1    # коміт скасовано, зміни лишилися в індексі (staged)
git reset HEAD~1           # (--mixed) зміни лишилися у файлах, але не staged
git reset --hard HEAD~1    # коміт і зміни видалено повністю

Найчастіше потрібен --soft чи --mixed: виправити повідомлення, додати забутий файл, розбити коміт. --hard знищує незбережену роботу.

Виправити лише останній коміт - git commit --amend: змінити повідомлення чи додати файли.

Коміт уже в спільній гілці - git revert створює новий коміт, що скасовує зміни старого. Історія не переписується, тож колегам нічого не ламається:

git revert a1b2c3d
git push

Головна відмінність: reset переписує історію (коміт ніби зник), revert додає до неї (коміт є, і є його скасування). Для main, з якого вже розгорнуто прод, - лише revert.

Для окремих файлів є git restore: git restore app/User.php - відкинути незакомічені зміни у файлі, git restore --staged app/User.php - прибрати з індексу.

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

git log без параметрів виводить довгий список комітів, у якому важко зорієнтуватися. Кілька параметрів роблять його зручним інструментом.

Компактний вигляд і граф гілок:

git log --oneline                     # хеш і заголовок в один рядок
git log --oneline --graph --all       # граф усіх гілок
git log --oneline -10                 # лише останні 10

Фільтри:

Параметр Що показує
--author="Olena" коміти автора
--since="2 weeks ago" за період (--until - до дати)
--grep="invoice" коміти, в повідомленні яких є слово
-- app/Models/User.php коміти, що змінювали файл
--follow -- file.php історія файлу з урахуванням перейменувань
main..feature коміти гілки feature, яких ще немає в main
-S "calculateTax" коміти, що додали чи видалили цей рядок коду

Зміни разом з історією:

git log -p -- app/Services/Billing.php   # diff кожного коміту
git log --stat                            # які файли й скільки рядків змінено

git show - один об'єкт детально:

git show a1b2c3d                 # повідомлення коміту й diff
git show HEAD~2 --stat           # коміт, на два раніше від поточного
git show main:config/app.php     # вміст файлу в гілці main, не перемикаючись

Власний формат:

git log --pretty=format:"%h %ad %an %s" --date=short

Практичні поради:

  • -S (пошук «кирками», pickaxe) - найшвидший спосіб з'ясувати, коли з'явилася чи зникла функція;
  • main..feature перед злиттям показує, що саме потрапить у main;
  • часто вживані варіанти варто оформити як аліас: git config --global alias.lg "log --oneline --graph --all".

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

git commit --amend замінює останній коміт новим: з виправленим повідомленням, доданими файлами чи обома змінами.

Виправити повідомлення:

git commit --amend -m "Fix invoice total rounding"

Додати забутий файл:

git add tests/Feature/InvoiceTest.php
git commit --amend --no-edit   # зберегти попереднє повідомлення

Змінити автора (наприклад, коміт зроблено з неправильною поштою):

git commit --amend --reset-author --no-edit

Що відбувається насправді: Git не змінює старий коміт - коміти незмінні. Він створює новий коміт з іншим хешем і переставляє на нього гілку. Старий коміт лишається в репозиторії (його видно в git reflog), доки його не прибере збирач сміття.

Коли --amend безпечний: коміт ще не відправлено у спільний репозиторій (git push) або гілка особиста й ніхто на неї не спирається.

Коли не можна: коміт уже в спільній гілці (main, гілка колеги). Після --amend локальна історія розходиться з віддаленою:

! [rejected]  main -> main (non-fast-forward)

Щоб відправити, доведеться робити force push - і колеги, що вже отримали старий коміт, матимуть конфлікти й дублікати. У спільній гілці замість --amend - новий коміт з виправленням.

У своїй гілці pull request після --amend використовуйте git push --force-with-lease - він не перезапише чужих змін, якщо вони з'явилися на сервері.

Виправити не останній, а давніший коміт - через git commit --fixup <хеш> і git rebase -i --autosquash, а не --amend.

Якщо --amend зроблено помилково: git reset --soft HEAD@{1} повертає гілку на попередній коміт, а зміни лишаються в індексі.

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

git cherry-pick бере зміни з указаного коміту й застосовує їх як новий коміт у поточній гілці.

git switch release/2.4
git cherry-pick a1b2c3d

Новий коміт має той самий diff і повідомлення, але інший хеш: в нього інший батько й час створення.

Типові сценарії:

  • виправлення в реліз: баг виправлено в main, і те саме виправлення потрібне в гілці підтримуваної версії;
  • коміт не в тій гілці: зроблено коміт у main замість гілки задачі - перенести його в потрібну гілку, а з main прибрати;
  • врятувати частину роботи з гілки, яку не будуть зливати.

Кілька комітів:

git cherry-pick a1b2c3d e4f5g6h      # окремі коміти
git cherry-pick main~3..main         # діапазон (без main~3)

Корисні параметри:

Параметр Що робить
-x додає в повідомлення рядок (cherry picked from commit ...) - видно походження
--no-commit (-n) застосувати зміни без коміту, щоб об'єднати кілька в один
--continue / --abort продовжити після конфлікту чи скасувати

Конфлікти розв'язуються як при злитті: виправити файли, git add, git cherry-pick --continue.

Чому не варто зловживати:

  • дублікати в історії: той самий зміст у двох комітах з різними хешами. Коли гілки згодом зіллються, Git зазвичай розпізнає однакові зміни, але інколи це дає конфлікти;
  • заміна злиттю: якщо доводиться переносити десятки комітів, правильніше злити гілку чи переглянути процес роботи з гілками.

Порада: для виправлень у кількох версіях використовуйте -x - тоді з історії релізної гілки видно, звідки прийшла кожна зміна.

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

  • git fetch завантажує нові коміти й гілки з віддаленого репозиторію і оновлює віддалені гілки (origin/main). Ваші локальні гілки й робочі файли не змінюються.
  • git pull = fetch + інтеграція змін у поточну гілку (за замовчуванням merge, або rebase, якщо так налаштовано).
git fetch
git log main..origin/main      # що нового на сервері, ще до злиття
git diff main origin/main      # які саме зміни
git merge origin/main          # або git rebase origin/main

Чому fetch корисний: спершу подивитися, що змінилося, і лише потім вирішити, як інтегрувати. pull робить усе одразу, і при конфлікті ви опиняєтеся посеред злиття без попередження.

Налаштування pull, яке варто знати:

git config --global pull.rebase true    # pull через rebase - без зайвих комітів злиття
git config --global pull.ff only        # або: лише перемотування, інакше помилка

Без явного налаштування сучасний Git при розбіжності гілок відмовляється робити pull і просить обрати стратегію.

Типова ситуація: «у мене не той код, що на сервері» - часто просто не зроблено fetch, і origin/main у вас застарілий.

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

Конфлікт виникає, коли обидві гілки змінили ті самі рядки (або одна змінила файл, а інша - видалила). Git не знає, яка версія правильна, і зупиняє злиття.

git merge feature
# CONFLICT (content): Merge conflict in app/Services/Billing.php
git status                       # список файлів з конфліктами

У файлі з'являються маркери:

<<<<<<< HEAD
$total = $subtotal * 1.2;
=======
$total = $subtotal + $this->tax($subtotal);
>>>>>>> feature

Що робити:

  1. Відредагувати файл: залишити потрібну версію, поєднати обидві чи написати третю. Прибрати всі маркери.
  2. Перевірити, що код працює: тести, запуск.
  3. Позначити конфлікт розв'язаним і завершити:
git add app/Services/Billing.php
git commit                      # для rebase: git rebase --continue

Передумали - повернути все як до злиття: git merge --abort (чи git rebase --abort).

Корисне:

  • git config --global merge.conflictStyle zdiff3 - маркери показують ще й спільного предка: видно не лише «що в кожній гілці», а й «що було до обох змін». Розв'язувати значно легше.
  • IDE (PhpStorm, VS Code) мають тристоронній редактор конфліктів.
  • Для файлів, які генеруються (composer.lock, package-lock.json), руками не мержать: беруть одну версію й заново виконують команду, що її створила.

Як конфліктів менше: короткоживучі гілки, часта інтеграція з main, невеликі pull request.

Докладніше в документації: git merge: розв'язання конфліктів

У Git три види гілок, і їх часто плутають:

Що Приклад Хто змінює
локальна гілка main, feature/export ви, комітами
віддалена гілка (remote-tracking) origin/main лише git fetch/pull/push - це «знімок» стану сервера на момент останньої синхронізації
гілка на сервері main у репозиторії на GitHub інші учасники через push

Upstream (відстежувана гілка) - зв'язок локальної гілки з віддаленою. Він дозволяє писати просто git push і git pull без параметрів і показує розбіжність:

git status
# Your branch is ahead of 'origin/main' by 2 commits.

Нова локальна гілка upstream не має - тому перший git push відповідає:

fatal: The current branch feature/export has no upstream branch.
To push the current branch and set the remote as upstream, use
    git push --set-upstream origin feature/export
git push -u origin feature/export   # -u = --set-upstream

Після цього git push і git pull працюють без аргументів.

Автоматизація: з git config --global push.autoSetupRemote true перший git push сам створює гілку на сервері й налаштовує upstream.

Корисні команди:

git branch -vv                       # усі гілки з їхнім upstream і розбіжністю
git branch -u origin/main            # задати upstream для поточної гілки
git switch feature/export            # якщо локальної гілки немає, а origin/feature/export є -
                                     # Git створить її з upstream автоматично
git fetch --prune                    # прибрати origin/* гілки, видалені на сервері

Типова плутанина: origin/main не оновлюється сам. Якщо git status каже «up to date with 'origin/main'», це означає «збігається з тим, що ви бачили під час останнього fetch», а не з поточним станом сервера.

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

Форк - ваша копія чужого репозиторію на GitHub. Через форк вносять зміни в проєкти, куди немає прав на запис: так працює більшість open source, включно з Laravel.

Схема з двома віддаленими репозиторіями:

git clone git@github.com:olena/framework.git          # ваш форк - це origin
cd framework
git remote add upstream https://github.com/laravel/framework.git
git remote -v
Remote Що це Права
origin ваш форк читання й запис
upstream оригінальний репозиторій лише читання

Робочий цикл:

git fetch upstream
git switch -c fix/route-cache upstream/13.x      # гілка від актуального стану оригіналу
# ... зміни, коміти ...
git push -u origin fix/route-cache               # у свій форк

Далі на GitHub - pull request з olena:fix/route-cache у laravel:13.x.

Синхронізація форку:

git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main

Або кнопка «Sync fork» на сторінці форку на GitHub, або gh repo sync.

Правила, що позбавляють проблем:

  • не комітьте в main форку - тримайте його точною копією upstream/main, а кожну зміну робіть в окремій гілці. Тоді синхронізація завжди fast-forward;
  • гілку для pull request створюйте від свіжого upstream/<цільова гілка>, а не від застарілого main форку;
  • якщо оригінал просунувся під час рев'ю - git fetch upstream && git rebase upstream/13.x і git push --force-with-lease;
  • цільова гілка має значення: у Laravel виправлення йдуть у гілку поточної версії, а нові можливості - в master; це описано в правилах внеску.

Дозвіл супровідникам («Allow edits by maintainers») на pull request дає їм змогу дописати дрібні виправлення прямо у вашу гілку.

Докладніше в документації: GitHub: синхронізація форку

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

         C   (ваш)
        /
A ── B
        \
         D   (origin/main)

Сучасний Git при git pull у такому разі не обирає спосіб сам, а просить вирішити:

hint: You have divergent branches and need to specify how to reconcile them.
fatal: Need to specify how to reconcile divergent branches.

Три варіанти:

Налаштування Що відбувається Результат
pull.rebase false злиття коміт злиття Merge branch 'main' of ...
pull.rebase true rebase ваш коміт C переноситься поверх D, історія лінійна
pull.ff only лише fast-forward при розбіжності pull відмовляється, і ви вирішуєте вручну
git config --global pull.rebase true
# або разово
git pull --rebase

Що обрати для локальних комітів у спільній гілці - зазвичай rebase:

  • ваші ще не відправлені коміти просто «перебудовуються» поверх свіжих змін;
  • історія не засмічується безглуздими комітами Merge branch 'main' of github.com:..., які нічого не означають, крім «ми з колегою працювали одночасно».

Коли rebase при pull не підходить:

  • ваші локальні коміти - це злиття іншої гілки (rebase їх розгорне, якщо не вказати --rebase=merges);
  • конфліктів дуже багато: rebase розв'язує їх коміт за комітом, злиття - один раз.

Корисно разом з rebase: git config --global rebase.autoStash true - незакомічені зміни автоматично ховаються в stash перед rebase й повертаються після.

Якщо щось пішло не так: git rebase --abort під час rebase, git reflog - після.

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

Git зберігає кожну версію кожного файлу назавжди. Видалений пізніше файл лишається в історії, і кожен клон завантажує його знову. Тому в репозиторій потрапляє лише те, що не можна відтворити.

vendor/ і node_modules/ відтворюються з composer.json + composer.lock і package.json + package-lock.json:

composer install
npm ci

Якщо їх комітити:

  • розмір: десятки тисяч файлів і сотні мегабайтів, а кожне оновлення пакета додає нові версії файлів в історію назавжди;
  • шум у diff і pull request: оновлення однієї залежності - тисячі змінених рядків, серед яких не видно власного коду;
  • конфлікти при злитті гілок, що оновлювали різні пакети;
  • бінарні частини (нативні модулі npm) скомпільовані під конкретну ОС і не працюватимуть на іншій.

Що ще не комітять:

Що Чому
.env секрети й налаштування конкретного середовища (комітять .env.example)
public/build, public/hot результат npm run build / dev-сервера
storage/logs, storage/framework/* логи, кеш, сесії, скомпільовані шаблони
bootstrap/cache/*.php кеш конфігурації й маршрутів
дампи бази, архіви, відео великі бінарні файли, часто з персональними даними
.idea/, .vscode/, .DS_Store налаштування конкретного розробника (краще в глобальний ignore)

Новий Laravel-проєкт уже містить відповідний .gitignore, а в каталогах storage/ - вкладені .gitignore, що зберігають структуру каталогів, але ігнорують вміст.

Що обов'язково комітять: composer.lock і package-lock.json для застосунку - вони гарантують однакові версії пакетів у всіх. (Для бібліотеки composer.lock зазвичай не комітять: версії визначає застосунок, що її встановлює.)

Якщо файл уже в репозиторії, додавання в .gitignore не допоможе - Git продовжує його відстежувати:

git rm -r --cached vendor
git commit -m "Stop tracking vendor"

З історії він при цьому не зникає - для цього потрібне переписування історії.

Докладніше в документації: Composer: чи комітити каталог vendor

Проблема: Git погано працює з великими бінарними файлами - макетами, відео, датасетами, моделями. Їх неможливо ефективно стиснути дельтами, кожна зміна додає повну нову копію, і кожен клон завантажує всі версії за всю історію.

Git LFS (Large File Storage) - розширення, яке зберігає великі файли окремо від репозиторію. У самому Git лишається лише маленький файл-вказівник:

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 52428800

А вміст файлу лежить на LFS-сервері (GitHub, GitLab, Bitbucket мають його вбудованим) і завантажується лише для потрібних версій при checkout.

Налаштування:

git lfs install                       # один раз на машині
git lfs track "*.psd" "*.mp4"         # які файли зберігати в LFS
git add .gitattributes                # правила записуються сюди
git add design/landing.psd
git commit -m "Add landing mockup"
# .gitattributes
*.psd filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text

Коли LFS доречний:

  • дизайн-файли, медіа, ігрові ресурси, які справді є частиною проєкту і мають версії;
  • тестові фікстури великого розміру;
  • великі файли, які мають змінюватися разом з кодом.

Коли не потрібен:

  • завантаження користувачів, бекапи, дампи - їм місце в об'єктному сховищі (S3, R2), а не в репозиторії взагалі;
  • результати збирання - їх відтворює CI;
  • невеликі зображення для сайту - звичайний Git впорається.

Що враховувати:

  • квоти: хостинги обмежують обсяг зберігання й трафік LFS, а кожен клон у CI витрачає трафік;
  • усі учасники мають встановити LFS - без нього в робочому каталозі будуть вказівники замість файлів;
  • перенести існуючі файли в LFS - це переписування історії: git lfs migrate import --include="*.psd";
  • блокування файлів (git lfs lock) - для форматів, які неможливо злити, щоб двоє не редагували один макет одночасно.

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

Поверхневий клон (shallow clone) завантажує не всю історію, а лише останні N комітів:

git clone --depth 1 https://github.com/laravel/laravel.git

Замість тисяч комітів - лише останній знімок файлів. Клон великого репозиторію з багаторічною історією стає в рази меншим і швидшим.

Де це доречно:

  • CI: для запуску тестів історія не потрібна - лише поточний код. actions/checkout у GitHub Actions за замовчуванням робить саме клон з глибиною 1;
  • збирання Docker-образів з репозиторію;
  • одноразове використання: подивитися код, зібрати проєкт, встановити інструмент.

Обмеження - все, що потребує історії:

Що Чому не працює
git log, git blame бачать лише завантажені коміти
git describe (версія з тегу) тегу в обрізаній історії немає
git merge-base, порівняння з main спільного предка не завантажено
git bisect немає історії для пошуку
інструменти, що аналізують зміни від main (лінтер лише змінених файлів) немає з чим порівнювати

Догрузити історію за потреби:

git fetch --depth 50          # поглибити до 50 комітів
git fetch --deepen 100        # ще на 100 глибше
git fetch --unshallow         # завантажити всю історію
git fetch --shallow-since=2026-01-01

Пов'язані параметри:

  • --single-branch - лише одна гілка (з --depth увімкнено автоматично);
  • --no-tags - без тегів.

Для постійної роботи розробника поверхневий клон незручний: багато команд поводяться несподівано, а push з поверхневого клону інколи відхиляється. Якщо проблема в розмірі - краще частковий клон (--filter=blob:none): історія комітів повна, а вміст старих файлів завантажується на вимогу.

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

Загальний розмір:

git count-objects -vH
count: 0
size: 0 bytes
in-pack: 48213
packs: 1
size-pack: 312.40 MiB

size-pack - скільки займають упаковані об'єкти, тобто фактичний розмір історії. count і size - ще не запаковані об'єкти; після git gc вони переходять у pack.

Що саме роздуває - найбільші об'єкти в історії:

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1 == "blob"' \
  | sort -k3 -n -r \
  | head -20

Список найбільших файлів за всю історію з їхніми шляхами - навіть тих, яких уже немає в поточному коді.

Типові знахідки:

  • дамп бази (backup.sql), закомічений «тимчасово» і потім видалений - у робочому каталозі його немає, а в історії він назавжди;
  • vendor/ чи node_modules/, які колись потрапили в репозиторій;
  • зібрані фронтенд-файли (public/build), що змінюються з кожним комітом;
  • медіафайли й архіви.

Зручний інструмент - git-sizer (від GitHub): аналізує репозиторій і повідомляє про проблеми - надто великі файли, дерева з тисячами файлів, надто довгі шляхи:

git-sizer --verbose

Що робити зі знайденим:

  • якщо файл ще в поточному коді - видалити, додати в .gitignore, великі ресурси перенести в LFS чи об'єктне сховище;
  • якщо лише в історії - розмір зменшить тільки переписування історії (git filter-repo), з усіма наслідками для команди. Часто простіше змиритися: для розробників допомагає частковий клон;
  • профілактика: перевірка розміру файлів у pre-commit чи CI, правила push у GitHub, що відхиляють файли понад ліміт (GitHub і так блокує файли понад 100 МБ).

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

Підмодуль - інший Git-репозиторій, вкладений у каталог вашого репозиторію. Головний репозиторій зберігає не файли підмодуля, а посилання на конкретний коміт.

git submodule add https://github.com/acme/docs.git docs

З'являються файл .gitmodules (адреса й шлях) і запис-вказівник у дереві:

[submodule "docs"]
    path = docs
    url = https://github.com/acme/docs.git

Клонування з підмодулями:

git clone --recurse-submodules https://github.com/acme/app.git
# або в уже існуючому клоні
git submodule update --init --recursive

Типові проблеми:

  • порожній каталог після клону - клонували без --recurse-submodules;
  • detached HEAD у підмодулі: submodule update переключає підмодуль на записаний коміт, а не на гілку. Коміти, зроблені там без перемикання на гілку, легко загубити;
  • оновлення - два кроки: зробити коміт і push у репозиторії підмодуля, а потім закомітити новий вказівник у головному репозиторії. Якщо забути другий крок, колеги отримають стару версію; якщо забути push - вказівник посилатиметься на коміт, якого немає на сервері;
  • git pull не оновлює підмодулі автоматично - потрібен git submodule update (чи git config submodule.recurse true);
  • конфлікти вказівників при злитті гілок, що оновили підмодуль по-різному;
  • CI і доступи: приватний підмодуль вимагає окремих прав на клонування.

Коли підмодулі доречні:

  • спільний код, що розвивається окремо, з власним циклом випусків, і потрібна точна версія;
  • документація чи контент в окремому репозиторії, яким керує інша команда.

Альтернативи для PHP-проєкту:

  • Composer-пакет (зокрема з path- чи vcs-репозиторієм) - для спільного PHP-коду майже завжди краще: версії, залежності, автозавантаження;
  • subtree - код копіюється в репозиторій з історією, без окремого клонування;
  • монорепозиторій - якщо код насправді тісно пов'язаний.

Корисні налаштування: git config --global submodule.recurse true і git config --global status.submoduleSummary true - Git сам оновлює підмодулі й показує їхні зміни в git status.

Докладніше в документації: Pro Git: підмодулі

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

Інші рівні
Middle 35 Senior 30

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