Senior: питання на співбесіді з теми «Внутрішня будова Git»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Логічно кожна версія файлу - окремий blob з повним вмістом. Новий об'єкт спершу записується як loose object - окремий файл у .git/objects/xx/, стиснений zlib. Для файлу на 20 КБ, зміненого в 100 комітах, це 100 майже однакових копій.
Packfile вирішує це на рівні зберігання. git gc (і передача по мережі) збирає об'єкти в один файл:
.git/objects/pack/
├── pack-4f2a....pack ← самі об'єкти
├── pack-4f2a....idx ← індекс: хеш → зміщення в pack
├── pack-4f2a....rev ← зворотний індекс (зміщення → позиція)
└── pack-4f2a....bitmap ← бітмапи досяжності (для швидкого clone/fetch)
Дельта-стиснення: частина об'єктів зберігається не повністю, а як дельта від іншого об'єкта - інструкції «скопіювати байти з базового об'єкта» й «вставити нові байти».
git verify-pack -v .git/objects/pack/pack-*.idx | sort -k3 -n | tail
# хеш тип розмір розмір-у-pack зміщення [глибина база]
Особливості, що відрізняють Git від «diff між версіями»:
- база обирається евристично, а не за історією: Git сортує об'єкти за типом, ім'ям файлу й розміром і шукає схожі у ковзному вікні. Дельта може будуватися від файлу з іншої гілки чи навіть з іншим ім'ям;
- зазвичай база - новіша версія, а старі зберігаються як дельти від неї: свіжі файли, які читають найчастіше, дістаються без розпакування ланцюжка;
- ланцюжки дельт обмежені глибиною (
pack.depth, за замовчуванням 50) - баланс між розміром і швидкістю читання; - логічна модель не змінюється: для всіх команд об'єкт - повний знімок, дельти - лише деталь зберігання.
Передача по мережі: при fetch і push сторони узгоджують, які коміти вже є в отримувача, і сервер формує тонкий pack (thin pack) - дельти можуть посилатися на об'єкти, що вже є в клієнта. Тому fetch після невеликих змін передає кілобайти.
Практичні наслідки:
- бінарні файли (зображення, архіви, дампи) погано стискаються дельтами - кожна версія займає майже повний розмір, і репозиторій росте назавжди. Звідси Git LFS;
git gc --aggressiveперебудовує дельти з більшим вікном - повільно, і для більшості репозиторіїв виграш мізерний;git count-objects -vHпоказує, скільки займають loose objects і pack-и: велика кількість loose objects - сигнал, що автоматичне обслуговування не запускається.
У коміті немає запису «файл перейменовано». Tree лише містить ім'я й хеш: у старому дереві є big.txt, у новому - big2.txt. Перейменування Git обчислює заново щоразу, коли показує diff, log чи виконує злиття.
Алгоритм:
- знайти пари «видалений файл» і «доданий файл»;
- точні збіги - однаковий хеш blob-а: це перейменування зі 100% схожістю (
R100). Перевіряється дешево; - для решти - оцінити схожість вмісту кожної пари і вважати перейменуванням, якщо вона вища за поріг. Поріг за замовчуванням - 50%.
git diff -M HEAD~1 --name-status
# R100 big.txt big2.txt
# R087 app/Helpers.php app/Support/Helpers.php
git diff -M80% HEAD~1 # суворіший поріг
git diff -C HEAD~1 # шукати ще й копії
git diff --no-renames HEAD~1 # показати як видалення + додавання
Чому git mv нічого не гарантує: він лише видаляє й додає файл в індексі. Якщо в тому самому коміті файл перейменовано і суттєво змінено, схожість опуститься нижче порогу - і Git покаже видалення одного файлу й створення іншого. Звідси практика: перейменування окремим комітом, зміни вмісту - наступним.
Де це впливає на роботу:
git log --follow fileстежить за історією файлу через перейменування - але лише для одного файлу й саме евристикою;git blameзнаходить рядки, переміщені з інших файлів, з-C;- злиття: якщо в одній гілці файл перейменовано, а в іншій змінено, стратегія
ortзнаходить перейменування і застосовує зміни до нового імені. Невдале визначення дає конфлікт «deleted by us / modified by them»; - ліміт кандидатів: порівняння - квадратична задача. Якщо видалених і доданих файлів забагато, Git пропускає неточне визначення (
diff.renameLimit,merge.renameLimit) і попереджає про це. Після масового переміщення файлів зручно підняти ліміт.
Плюс підходу: перейменування «знаходиться» навіть якщо його зробили без git mv - просто через IDE чи mv - і навіть ретроспективно для старої історії, коли алгоритм покращується з новими версіями Git.
Ідентифікатори всіх об'єктів Git - хеші SHA-1 (160 біт, 40 шістнадцяткових символів). У 2017 році атака SHAttered показала практичну колізію SHA-1 - два різні PDF-файли з однаковим хешем. Згодом атаки з вибраним префіксом стали ще дешевшими.
Чим це загрожує Git: зловмисник теоретично може створити два об'єкти з однаковим хешем - безпечний для рецензії і шкідливий - і підмінити один іншим. Уся модель цілісності Git тримається на тому, що хеш однозначно визначає вміст.
Що зроблено вже зараз:
- Git використовує SHA-1 з виявленням колізій (sha1collisiondetection) - об'єкти, побудовані відомими методами атаки, розпізнаються й відхиляються;
- підтримка SHA-256 у Git з версії 2.29 (ще позначена як експериментальна, хоча вже стабільна для використання):
git init --object-format=sha256
git rev-parse --show-object-format # sha1 або sha256
Хеші в такому репозиторії - 64 символи.
Чому перехід повільний:
- SHA-1 і SHA-256 репозиторії несумісні: не можна напряму зробити
fetchчиpushміж ними. План переходу передбачає режим сумісності з таблицею відповідності хешів, але він ще не завершений; - хостинги: підтримка SHA-256 у GitHub, GitLab та інших з'являється поступово - перед створенням такого репозиторію треба перевірити, чи його приймає ваш сервер, CI і інструменти;
- екосистема: інструменти, що вважають хеш 40-символьним рядком (регулярні вирази, колонки в базах, парсери логів), ламаються;
- конвертація наявного репозиторію змінює всі хеші - посилання на коміти в задачах, changelog, документації стають недійсними.
Практичні висновки:
- для звичайних проєктів SHA-1 з виявленням колізій зараз достатній - реальна загроза потребує атаки на конкретний репозиторій і значних ресурсів;
- не покладайтеся на довжину хешу у власних інструментах: використовуйте
git rev-parse --show-object-formatі не обрізайте хеші жорстко до 40 символів; - для перевірки походження важливіші підписи комітів і тегів - їх теж стосується проблема SHA-1, тому сучасні підписи (SSH, GPG) хешують вміст власними алгоритмами;
- нові репозиторії на SHA-256 - розумний вибір для внутрішніх систем, де ви контролюєте всі інструменти; для відкритих проєктів поки що краще дочекатися повної підтримки хостингів.
Коміт неможливо змінити: будь-яка зміна, навіть у повідомленні, дає новий хеш. Але інколи до вже опублікованого коміту треба «дописати» інформацію: результат CI, посилання на рецензію, час деплою, заміток для розслідування.
git notes зберігає примітки окремо від комітів:
git notes add -m "Deployed to production 2026-10-04 14:20" a1b2c3d
git notes append -m "Rolled back: memory leak" a1b2c3d
git log -1 a1b2c3d
# commit a1b2c3d...
# ...
# Notes:
# Deployed to production 2026-10-04 14:20
# Rolled back: memory leak
Як це влаштовано: примітки - звичайні об'єкти Git в окремій гілці refs/notes/commits. Ця гілка містить коміти з деревом, де ім'я файлу - хеш коміту, до якого примітка, а вміст - текст. Тобто це історія приміток з власними комітами, а сам коміт-ціль не змінюється.
Простори імен - різні види метаданих окремо:
git notes --ref=ci add -m "build #4812: passed" HEAD
git notes --ref=deploy add -m "prod 2026-10-04" HEAD
git log --notes=ci --notes=deploy
Головна незручність - примітки не передаються за замовчуванням. git push і git fetch працюють лише з гілками й тегами, тож refs/notes/* треба вказати явно:
git push origin 'refs/notes/*'
git fetch origin 'refs/notes/*:refs/notes/*'
Або додати refspec у конфігурацію remote. GitHub давно не показує примітки в інтерфейсі, тому вони корисні передусім для внутрішніх інструментів.
Ще деталі:
- rebase і amend створюють нові коміти - примітки старих не переносяться, якщо не налаштувати
notes.rewriteRef; - злиття приміток з різних джерел - окрема команда
git notes mergeзі стратегіямиunion,cat_sort_uniq; - примітки не входять у хеш і не захищені підписом коміту - не використовуйте їх для чогось, що має бути незмінним.
Де notes доречні:
- CI записує результат збирання чи покриття до коміту;
- інструмент деплою позначає, які коміти й коли потрапили в продакшен;
- проєкти, що приймають патчі поштою, додають посилання на обговорення;
- особисті нотатки при розслідуванні історії.
Альтернативи: trailer-рядки в повідомленні (Reviewed-by:, Refs: #142) - якщо інформація відома до коміту; теги - для позначення конкретних точок; зовнішні системи (CI, трекер) - якщо метадані не мають жити разом з репозиторієм.