Junior: питання на співбесіді з теми «Великі репозиторії й продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
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.