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

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

Докладніше в документації: 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: підмодулі