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

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

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

35 питань

git rebase -i відкриває список комітів гілки, де кожному можна задати дію. Так історію з «wip», «fix typo», «ще фікс» перетворюють на кілька змістовних комітів.

git rebase -i main
pick a1b2c3d Add invoice model
squash d4e5f6a fix typo
pick 9f8e7d6 Send invoice email
fixup 1a2b3c4 forgot import
reword 5d6e7f8 wip

Основні дії:

  • pick - лишити коміт як є.
  • reword - змінити повідомлення.
  • squash - злити з попереднім, об'єднавши повідомлення.
  • fixup - злити з попереднім, відкинувши повідомлення.
  • edit - зупинитися на коміті, щоб змінити його чи розбити на кілька.
  • drop або видалити рядок - прибрати коміт.
  • Переставити рядки - змінити порядок комітів.

Зручний прийом - fixup-коміти:

git commit --fixup=a1b2c3d     # «виправлення до коміту a1b2c3d»
git rebase -i --autosquash main # сам поставить fixup у потрібне місце

Навіщо: рев'юеру легше читати кілька логічних комітів, ніж двадцять випадкових; git bisect і git blame працюють точніше; коміт можна відкотити цілком.

Обережно: це переписування історії. Лише для своєї гілки, а потім - git push --force-with-lease. Якщо щось пішло не так, git rebase --abort повертає все як було, а після завершення врятує git reflog.

Багато команд обходяться без цього, вливаючи PR через squash merge - тоді вся гілка стає одним комітом у main.

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

git bisect шукає коміт, що вніс помилку, двійковим пошуком. Ви називаєте один «поганий» коміт (де баг є) і один «хороший» (де його ще не було), а Git щоразу переходить на середину проміжку й питає, чи є там баг.

git bisect start
git bisect bad                 # поточний коміт - зламаний
git bisect good v2.3.0         # у цьому релізі все працювало

# Git перемикається на коміт посередині
# перевіряєте й кажете:
git bisect good    # або
git bisect bad

# ...через кілька кроків:
# a1b2c3d is the first bad commit

git bisect reset               # повернутися, звідки почали

Серед 1000 комітів винуватця знайдено за ~10 перевірок.

Автоматично - якщо є команда, що відповідає «добре/погано» кодом виходу:

git bisect run php artisan test --filter=InvoiceTotalTest

Код 0 - коміт хороший, 1-127 (крім 125) - поганий, 125 - пропустити (наприклад, коміт не збирається).

Що допомагає bisect:

  • Невеликі коміти, кожен з яких працює. Якщо коміт «напівготовий» і падає з іншої причини, bisect заплутається - такі коміти пропускають (git bisect skip).
  • Тест, що відтворює баг, - його пишуть першим, а потім ганяють через bisect run.

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

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

Звичайний git rebase main переносить усі коміти поточної гілки, яких немає в main. Але інколи потрібно перенести лише частину комітів чи змінити основу на зовсім іншу гілку. Для цього є --onto:

git rebase --onto <нова-основа> <стара-основа> <гілка>

Git бере коміти з діапазону стара-основа..гілка і відтворює їх поверх нова-основа.

Сценарій 1 - гілку створено не від тієї гілки. Гілку feature почали від develop, а треба було від main:

main ── A
         \
develop   B ── C
                \
feature          D ── E
git rebase --onto main develop feature
main ── A ── D' ── E'    (feature)

Коміти B і C з develop у feature не потрапили.

Сценарій 2 - залежні pull request. Гілка part-2 побудована поверх part-1. Після злиття part-1 через squash її коміти в main мають інші хеші, і звичайний git rebase main намагатиметься застосувати їх повторно з конфліктами:

git rebase --onto main part-1 part-2

Переноситься лише власна робота part-2.

Сценарій 3 - видалити коміти з середини гілки:

git rebase --onto HEAD~5 HEAD~3

Коміти HEAD~4 і HEAD~3 зникають, решта переноситься на HEAD~5.

Що пам'ятати:

  • як і будь-який rebase, --onto створює нові коміти з новими хешами - для вже опублікованої спільної гілки це переписування історії;
  • якщо заплуталися - git rebase --abort під час процесу чи git reflog після нього;
  • --update-refs (Git 2.38+) оновлює й проміжні гілки в ланцюжку - зручно для стопки залежних гілок.

Докладніше в документації: git rebase: перенесення гілки з --onto

Злити гілку feature у main можна трьома способами, і від вибору залежить вигляд історії.

Fast-forward - можливий, коли main не просунувся після створення feature. Git просто пересуває вказівник main на останній коміт feature, нового коміту не створюється:

до:     main ── A                після:  A ── B ── C   (main, feature)
                 \
                  B ── C (feature)

Історія лінійна, але факт існування гілки зникає: не видно, які коміти були однією задачею.

--no-ff - завжди створює коміт злиття, навіть коли fast-forward можливий:

git merge --no-ff feature
A ────────── M   (main)
 \          /
  B ── C ──      (feature)

Видно межі задачі; відкотити всю функціональність можна одним git revert -m 1 M. Ціна - більше комітів злиття в історії.

Squash - усі зміни гілки стають одним новим комітом в main:

git merge --squash feature
git commit -m "Add invoice export"

Історія main чиста: одна задача - один коміт. Але:

  • проміжні коміти гілки в main не потрапляють - деталізація для git bisect і blame втрачається;
  • Git не вважає feature злитою (git branch --merged її не покаже), а git branch -d feature відмовиться її видаляти;
  • якщо продовжити роботу в тій самій гілці, наступний squash може дати конфлікти з уже злитими змінами.

Порівняння:

Fast-forward --no-ff Squash
коміт злиття ні так ні
проміжні коміти в main так так ні
межі задачі видно ні так так (один коміт)

Налаштування: git config merge.ff only дозволяє лише fast-forward (злиття впаде, якщо воно неможливе), pull.ff only - те саме для git pull. На GitHub і GitLab спосіб злиття pull request обирається в налаштуваннях репозиторію.

Докладніше в документації: git merge: fast-forward

git blame показує для кожного рядка файлу, в якому коміті й ким він востаннє змінений:

git blame app/Services/Billing.php
git blame -L 40,60 app/Services/Billing.php    # лише рядки 40-60
a1b2c3d4 (Olena 2026-03-14 41) $total = round($subtotal * 1.2, 2);

Мета - не «кого звинуватити», а «чому так»: знайдений хеш веде до коміту з повідомленням, pull request і обговоренням, де видно причину рішення.

git show a1b2c3d4

Проблема: шумні коміти. Масове форматування (Pint, Prettier), перейменування чи перенесення коду роблять автором кожного рядка «коміт форматування», і справжня історія ховається.

Що допомагає:

Параметр Що робить
-w ігнорує зміни пробілів
-M розпізнає рядки, переміщені в межах файлу
-C розпізнає рядки, перенесені з інших файлів того самого коміту (-C -C -C - з будь-якого коміту)
--ignore-rev <хеш> пропустити конкретний коміт і показати попереднього автора

Файл ігнорованих комітів - рішення для всієї команди:

# .git-blame-ignore-revs
# Pint: форматування всього проєкту
3f9e1a2b7c...
git config blame.ignoreRevsFile .git-blame-ignore-revs

GitHub автоматично враховує файл .git-blame-ignore-revs у корені репозиторію у своєму інтерфейсі blame.

Коли рядок змінювався багато разів:

  • git log -L 40,60:app/Services/Billing.php - вся історія змін саме цього фрагмента, з diff кожного кроку;
  • git log -S "1.2" - коміти, що додали чи прибрали конкретний вираз;
  • в IDE blame вбудований («Annotate with Git Blame» у PhpStorm) з можливістю переходити до попередньої ревізії рядка.

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

Після rebase чи commit --amend історія локальної гілки розходиться з віддаленою, і звичайний git push відмовляється. git push --force перезаписує віддалену гілку вашою версією безумовно.

Чим це небезпечно: якщо колега встиг відправити в цю гілку свої коміти, force push їх знищить на сервері - ви навіть не побачите, що вони там були.

--force-with-lease перезаписує гілку, лише якщо вона на сервері досі в тому стані, який ви бачили під час останнього fetch. Якщо хтось додав коміти - push відхиляється, і ви спершу інтегруєте їхні зміни.

git rebase main
git push --force-with-lease

Пастка: «той стан, що ви бачили» - це ваш origin/feature. Якщо ви зробили git fetch (чи IDE зробила його у фоні), не подивившись, що прийшло, lease вважатиме нові чужі коміти «баченими». Суворіший варіант - --force-if-includes (Git 2.30+), який вимагає, щоб віддалені коміти були інтегровані в вашу гілку.

Правила команди:

  • Force push лише у свої гілки. У main, develop, релізні гілки - ніколи.
  • Захищені гілки на GitHub/GitLab забороняють force push і вимагають pull request з рев'ю.
  • Якщо над гілкою працює кілька людей - домовитися перед переписуванням історії або обійтися merge.

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

GitHub Flow - найпоширеніший для веб-застосунків:

  • main завжди готовий до деплою;
  • кожна задача - коротка гілка від main;
  • pull request, рев'ю, CI → злиття в main → деплой.

Простий і добре працює з безперервним розгортанням.

Git Flow - для продуктів з версіями й релізами:

  • main - лише релізи, develop - інтеграція;
  • feature/* від develop, release/* для підготовки версії, hotfix/* від main для термінових виправлень.

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

Trunk-based development - усі інтегрують зміни в головну гілку дуже часто, щодня чи кілька разів на день:

  • гілки живуть години, а не тижні (або коміт одразу в trunk);
  • незавершені фічі ховають за feature flags, а не тримають у гілці;
  • потрібні сильні автотести й CI.

Мінімум конфліктів злиття і швидкий зворотний зв'язок, але потребує дисципліни й інфраструктури.

Як обирати: веб-продукт з частими деплоями - GitHub Flow чи trunk-based. Кілька підтримуваних версій - щось ближче до Git Flow. Головне - не модель, а короткоживучі гілки: чим довше гілка живе окремо, тим болючіше її злиття.

Докладніше в документації: Робочі процеси з гілками

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

git worktree add ../app-hotfix hotfix/invoice-rounding
git worktree add -b review/pr-412 ../app-review origin/feature/export
git worktree list
git worktree remove ../app-hotfix

Сценарій: ви посеред великої задачі з десятком змінених файлів, і терміново потрібно виправити баг у продакшені.

  • через stash: сховати зміни, перемкнутися, виправити, повернутися, дістати зі stash. Каталог vendor/ і node_modules/ можуть не відповідати гілці, запущений npm run dev перезбирає не той код, IDE переіндексовує проєкт;
  • через worktree: відкрити окремий каталог з гілкою виправлення. Основна робота лишається недоторканою, можна навіть паралельно запустити тести в обох каталогах.

Коли worktree зручний:

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

Обмеження:

  • одна гілка - один worktree: не можна перемкнутися на гілку, вже відкриту в іншому worktree (Git захищає від двох незалежних змін однієї гілки);
  • ігноровані файли не спільні: vendor/, node_modules/, .env у новому каталозі відсутні - їх доведеться встановити чи скопіювати;
  • для Laravel: окремий каталог означає окремий .env; якщо обидва worktree використовують ту саму базу, міграції однієї гілки зачеплять іншу;
  • видаляйте worktree через git worktree remove, а не rm -rf - інакше лишаються записи (git worktree prune їх прибирає).

Порівняно з другим клоном worktree не дублює історію (економить місце й час) і бачить локальні гілки й коміти основного репозиторію без fetch.

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

Поки ви працюєте над задачею, main просувається. Чим довше гілка живе окремо, тим більше конфліктів і тим вищий ризик, що код, який працює в гілці, зламається після злиття. Тому гілку періодично оновлюють.

Варіант 1 - злити main у гілку:

git fetch origin
git merge origin/main
  • історія гілки не переписується - безпечно, навіть якщо в гілці працюють кілька людей;
  • push без force;
  • конфлікти розв'язуються один раз;
  • в історії з'являються коміти злиття Merge branch 'main' into feature - при squash-злитті pull request вони зникнуть.

Варіант 2 - rebase на main:

git fetch origin
git rebase origin/main
git push --force-with-lease
  • лінійна історія: гілка виглядає так, ніби її почали щойно;
  • коміти отримують нові хеші, потрібен force push;
  • конфлікти розв'язуються для кожного коміту окремо - на довгій гілці це може повторюватися (допомагає rerere);
  • небезпечно, якщо гілкою користуються інші: їхні локальні копії розійдуться з вашою.

Як обрати:

Ситуація Що краще
особиста гілка, коміти ще не рев'ювали rebase
у гілці працюють кілька людей merge
pull request уже на рев'ю merge - рецензенти бачать, що змінилося після їхніх коментарів (після rebase GitHub гірше показує різницю)
злиття в main буде через squash будь-який, історія гілки все одно зникне

Як часто: невеликими кроками - щодня чи перед кожним push, а не раз на тиждень. Конфлікт у двох файлах розв'язати легше, ніж у двадцяти.

Найкращий захист від болісного оновлення - короткоживучі гілки: задача на 1-3 дні дає значно менше розбіжностей, ніж гілка, що живе місяць.

На GitHub кнопка «Update branch» у pull request робить merge (чи rebase, якщо обрати). Branch protection з вимогою «Require branches to be up to date» змушує оновлювати гілку перед злиттям.

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

Lock-файли змінюються майже при кожній зміні залежностей, і дві гілки, що додали по пакету, гарантовано конфліктують. Розв'язувати такий конфлікт вручну, редагуючи JSON, не можна: у composer.lock є content-hash - хеш вмісту composer.json, і ручне злиття майже завжди дає невідповідність або неузгоджене дерево залежностей.

Правильний спосіб для Composer:

  1. розв'язати конфлікт у composer.json - він короткий і зрозумілий людині;
  2. взяти composer.lock з основної гілки;
  3. повторити зміни своєї гілки командою Composer:
git checkout --theirs composer.lock     # при rebase - --ours, див. нижче
composer update vendor/added-package    # лише пакети, які додала ваша гілка
# або, якщо змінювали лише composer.json без нових пакетів:
composer update --lock
git add composer.json composer.lock

composer update лише для конкретних пакетів, а не всіх: повний composer update оновить усе дерево, і pull request раптом міститиме десятки чужих оновлень.

Для npm:

git checkout --theirs package-lock.json
npm install            # перебудує lock з урахуванням вашого package.json

npm також вміє сам розв'язувати конфлікти в package-lock.json: якщо запустити npm install при конфлікті лише в lock-файлі, він об'єднує обидві версії.

Увага до --ours і --theirs при rebase: під час rebase ролі міняються місцями - --ours означає гілку, на яку переносите (тобто main), а --theirs - ваші коміти. Тому при rebase версія main - це --ours.

Як зменшити кількість конфліктів:

  • оновлювати залежності окремими невеликими pull request, а не в гілках задач;
  • автоматизовані оновлення (Dependabot, Renovate) з частим злиттям;
  • перед роботою над задачею оновити гілку з main.

Перевірка після розв'язання:

composer validate          # чи відповідає lock-файл composer.json
composer install           # чи встановлюється дерево
php artisan test

Чого не робити: додавати lock-файли в .gitignore, щоб «не конфліктували». Для застосунку lock-файл - гарантія, що продакшен, CI і колеги отримують однакові версії пакетів.

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

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

git clone --filter=blob:none --sparse https://github.com/acme/monorepo.git
cd monorepo
git sparse-checkout set apps/billing packages/ui
git sparse-checkout add packages/auth
git sparse-checkout list
git sparse-checkout disable       # повернути всі файли

Режим cone (за замовчуванням) - шаблони лише на рівні каталогів:

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

Що це дає:

  • git status, git checkout, git switch працюють з меншою кількістю файлів - значно швидше;
  • IDE індексує лише потрібний код;
  • менше місця на диску.

Найкраще - разом з частковим клоном (--filter=blob:none): тоді вміст файлів поза sparse-набором навіть не завантажується з сервера.

Як інші операції поводяться з невидимими файлами:

  • коміти, злиття й rebase працюють з усім деревом - Git знає про всі файли;
  • конфлікт у файлі поза sparse-набором - Git тимчасово розміщує файл у робочому каталозі для розв'язання;
  • git grep і git log -- path можуть шукати й поза набором.

Типове застосування:

  • монорепозиторій: розробник фронтенду бачить лише apps/web і спільні пакети;
  • CI: збирання конкретного сервісу без виписування всього репозиторію - разом з --depth 1 і --filter;
  • документація в репозиторії з великим кодом.

Обмеження:

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

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

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

Поверхневий клон (--depth 1) обрізає історію: комітів до певної глибини просто немає.

Частковий клон (--filter) завантажує всю історію комітів, але не всі об'єкти - пропущені догружаються з сервера на вимогу:

git clone --filter=blob:none https://github.com/acme/app.git       # blobless
git clone --filter=tree:0 https://github.com/acme/app.git          # treeless
git clone --filter=blob:limit=1m https://github.com/acme/app.git   # без файлів понад 1 МБ
Поверхневий Без blob-ів (blob:none) Без дерев (tree:0)
коміти лише останні усі усі
дерева (каталоги) лише останні усі на вимогу
вміст файлів лише останні лише для checkout, решта на вимогу на вимогу
git log обрізаний повний повний
git log -p, blame обмежені працюють, догружаючи файли повільні, багато догрузок
для кого CI, одноразові збирання розробники CI, якому потрібна історія комітів

Чому blobless клон - добрий вибір для розробника великого репозиторію:

  • історія повна: log, merge-base, describe, перемикання гілок працюють як звичайно;
  • старі версії великих файлів не завантажуються, поки не знадобляться;
  • git blame чи перегляд старого коміту догружають потрібні об'єкти - помітна, але одноразова затримка.

Чому поверхневий клон - для CI: найменший обсяг і жодних мережевих запитів пізніше. Але все, що потребує історії, не працює.

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

Що потрібно:

  • підтримка на сервері (GitHub, GitLab, Bitbucket підтримують);
  • доступ до сервера при операціях, що потребують відсутніх об'єктів - офлайн частина команд не працюватиме;
  • не поєднувати бездумно з --depth - зазвичай обирають одне.

Разом зі sparse-checkout частковий клон дає найкращий ефект для монорепозиторію: не завантажуються ні старі версії, ні файли з чужих каталогів.

Докладніше в документації: GitHub Blog: partial clone і shallow clone

Кілька Laravel-застосунків використовують спільний код: модуль авторизації, клієнт API, набір Blade-компонентів. Є три основні способи.

1. Submodule - посилання на коміт іншого репозиторію:

git submodule add git@github.com:acme/shared.git packages/shared
  • точна версія, окрема історія, окремі права доступу;
  • незручно: додаткові кроки при клонуванні й оновленні, detached HEAD, легко забути оновити вказівник.

2. Subtree - код копіюється в репозиторій разом з історією:

git subtree add --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree pull --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree push --prefix=packages/shared git@github.com:acme/shared.git feature-x
  • для інших учасників це просто звичайні файли - клон працює без додаткових кроків;
  • зміни можна відправити назад в оригінальний репозиторій;
  • команди довгі, легко змішати в одному коміті зміни спільного й основного коду;
  • історія основного репозиторію росте.

3. Composer-пакет - стандартний спосіб для PHP:

{
    "repositories": [
        { "type": "vcs", "url": "git@github.com:acme/shared.git" }
    ],
    "require": { "acme/shared": "^2.1" }
}
  • семантичні версії й діапазони, власні залежності пакета, автозавантаження, сервіс-провайдер Laravel;
  • оновлення - composer update acme/shared, а composer.lock фіксує точну версію;
  • приватні пакети - через Private Packagist, Satis чи vcs-репозиторій з доступом.

Для локальної розробки пакета разом із застосунком - path-репозиторій:

{ "repositories": [{ "type": "path", "url": "../shared" }] }

Composer створює символьне посилання - зміни в пакеті видно одразу.

Порівняння:

Submodule Subtree Composer
версіонування коміт коміт семантичні версії
залежності пакета ні ні так
простота для команди низька висока висока
зміни в обидва боки так так (subtree push) у репозиторії пакета

Для PHP-коду Composer майже завжди кращий. Submodule і subtree доречні для того, що не є PHP-пакетом: документація, спільні конфіги, ресурси. А якщо спільний код змінюється разом із застосунками постійно, - можливо, вам потрібен монорепозиторій.

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

Монорепозиторій - кілька проєктів (застосунки, пакети, сервіси) в одному репозиторії. Polyrepo - кожен проєкт в окремому репозиторії.

Переваги монорепозиторію:

  • атомарні зміни: змінити API пакета й усіх його споживачів одним комітом і одним pull request. У polyrepo це кілька узгоджених релізів у правильному порядку;
  • немає пекла версій: усі проєкти завжди використовують поточну версію спільного коду;
  • спільні інструменти: одна конфігурація Pint, PHPStan, CI, однакові правила;
  • видимість: легко знайти всі використання функції, зробити рефакторинг по всьому коду;
  • простіший онбординг: один клон - увесь контекст.

Проблеми монорепозиторію:

  • розмір і продуктивність Git: status, clone, checkout повільнішають (лікується частковим клоном, sparse-checkout, fsmonitor);
  • CI: запускати все на кожну зміну - дорого. Потрібні інструменти, що визначають, які проєкти зачеплені зміною (Nx, Turborepo, Bazel, Pants чи власні скрипти по git diff);
  • права доступу: Git не обмежує доступ до каталогів - бачать усі все (CODEOWNERS лише керує рев'ю);
  • зв'язність: легко створити залежності, яких не мало б бути, - потрібні правила меж між модулями.

Переваги polyrepo:

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

Проблеми polyrepo:

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

Гібрид, яким користується Laravel: розробка в монорепозиторії laravel/framework, а компоненти автоматично розділяються в окремі репозиторії лише для читання (illuminate/database, illuminate/support), щоб їх можна було встановити окремо.

Як обрати:

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

Докладніше в документації: monorepo.tools

З часом у репозиторії накопичуються незапаковані об'єкти (кожен новий коміт створює окремі файли), недосяжні об'єкти (після rebase, reset, видалення гілок) і багато pack-файлів після кожного fetch. Це сповільнює операції й займає місце.

git gc (garbage collection):

  • пакує окремі об'єкти в pack-файли з дельта-стисненням;
  • видаляє недосяжні об'єкти, старші за термін (за замовчуванням 2 тижні, і лише ті, на які не посилається reflog - а reflog зберігає записи 90 днів, недосяжні - 30);
  • упаковує посилання в packed-refs, оновлює commit-graph.
git gc                # звичайне прибирання
git gc --aggressive   # повільне глибоке перепакування - рідко потрібне
git gc --prune=now    # видалити недосяжні об'єкти негайно

git gc --auto Git запускає сам після деяких команд (commit, merge, fetch), коли незапакованих об'єктів понад 6700 чи pack-файлів понад 50. Тому вручну запускати gc зазвичай не потрібно.

Проблема автоматичного gc у великих репозиторіях: він запускається посеред роботи й може блокувати на хвилини.

git maintenance - сучасна заміна: обслуговування у фоні за розкладом:

git maintenance start

Реєструє репозиторій і задачі в системному планувальнику (launchd на macOS, systemd чи cron на Linux, Task Scheduler на Windows):

Задача Частота Що робить
prefetch щогодини фоновий fetch у спеціальні ref-и - ваш git fetch потім майже миттєвий
commit-graph щогодини оновлює граф комітів для швидкого log і злиттів
loose-objects щодня пакує окремі об'єкти
incremental-repack щодня поступово об'єднує pack-файли без повного перепакування
git maintenance run --task=gc     # вручну конкретну задачу
git maintenance stop

Коли запускати вручну:

  • після видалення великих файлів з історії (git filter-repo) - git gc --prune=now, щоб звільнити місце;
  • після масового імпорту чи міграції репозиторію;
  • для невеликого проєкту нічого робити не треба - автоматичного gc достатньо.

На сервері (GitHub, GitLab) обслуговування виконує хостинг.

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

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

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

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