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 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 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 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 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 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 не знає, яка версія правильна, і зупиняє злиття.
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
Що робити:
- Відредагувати файл: залишити потрібну версію, поєднати обидві чи написати третю. Прибрати всі маркери.
- Перевірити, що код працює: тести, запуск.
- Позначити конфлікт розв'язаним і завершити:
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», а не з поточним станом сервера.
Форк - ваша копія чужого репозиторію на 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 дає їм змогу дописати дрібні виправлення прямо у вашу гілку.
Ситуація: ви зробили коміт у 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 зберігає кожну версію кожного файлу назавжди. Видалений пізніше файл лишається в історії, і кожен клон завантажує його знову. Тому в репозиторій потрапляє лише те, що не можна відтворити.
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) - для форматів, які неможливо злити, щоб двоє не редагували один макет одночасно.
Поверхневий клон (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 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-репозиторій, вкладений у каталог вашого репозиторію. Головний репозиторій зберігає не файли підмодуля, а посилання на конкретний коміт.
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.
Питання рівня Junior з реальних технічних співбесід - 35 питань у 7 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 7 відкритих вакансій рівня Junior. Переглянути вакансії