Git під капотом — що насправді лежить у папці .git
Дерев'яна картотека з ящиками та картками як метафора об'єктної бази Git
Офіційна документація Git описує його так: "git — the stupid content tracker" — "тупий трекер вмісту". Це не жарт спільноти, це буквально перший рядок man-сторінки, який написав сам Лінус Торвальдс. І в цьому самоприниженні — головна інсайдерська правда про Git: всередині він влаштований настільки просто, що це майже образливо.
Більшість людей роками користуються git add і git commit як заклинаннями, боячись зазирнути під капот. Даремно: під капотом там не двигун — там картотека з трьох типів карток. Сьогодні розберемо її повністю, і після цієї статті команди Git перестануть бути магією назавжди.
Якщо ти ще не користуєшся Git — спочатку пройди урок з основ, ця стаття буде другим поверхом знань.
Звідки взявся "тупий трекер"
Квітень 2005 року. Проєкт Linux втрачає ліцензію на комерційну систему контролю версій BitKeeper. Лінус Торвальдс, замість шукати заміну, пише власну систему — перший робочий прототип Git з'явився за кілька днів, а вже 7 квітня 2005 Лінус зробив перший коміт. У README він описав своє творіння як "information manager from hell" — "інформаційний менеджер з пекла".
Секрет швидкості розробки: Лінус не будував "систему контролю версій". Він побудував файлову систему з адресацією за вмістом (content-addressable filesystem), а вже поверх неї — тонку обгортку з команд. Ця структура даних не змінилася з першого коміту до сьогодні.
Відкриваємо капот
Зроби собі пісочницю — це займе хвилину:
mkdir git-lab && cd git-lab
git init
ls -a .git
Побачиш приблизно таке:
HEAD config description hooks/ info/ objects/ refs/
Нас цікавлять два підкаталоги: objects/ — база даних усього і refs/ — закладки. Все інше — службова обв'язка. Так, вся історія твого проєкту, всі гілки, всі версії всіх файлів — це objects/ і refs/.
Об'єкт №1: Blob — вміст файлу
Створимо перший файл і закомітимо:
echo "перший рядок" > notes.txt
git add notes.txt
git commit -m "початок"
Тепер зазирнемо в базу:
ls -R .git/objects
# 3f/9a01... 84/1622... d1/4c8b...
З'явилися три об'єкти. Кожен названий SHA-1 хешем власного вмісту — 40 шістнадцяткових символів (перші два — назва папки, решта 38 — ім'я файлу). Роздивитися будь-який об'єкт можна командою git cat-file:
git cat-file -p 3f9a01 # достатньо перших символів хеша
# перший рядок
Це blob — просто вміст файлу, стиснутий zlib. Зверни увагу на дві речі:
- Імені файлу в blob немає. Git зберігає лише вміст. Два файли з однаковим вмістом — один blob.
- Хеш обчислюється з вмісту. Зміниш один символ — отримаєш зовсім інший хеш, тобто зовсім інший об'єкт.
Друге — фундаментально: у Git неможливо "тихо" підмінити вміст, не змінивши адресу. Саме тому Git гарантує цілісність історії — це криптографічна властивість, а не адміністративна.
Об'єкт №2: Tree — знімок папки
Але хтось же має пам'ятати, що blob 3f9a01... називався notes.txt? Це робить tree — об'єкт-каталог:
git cat-file -p HEAD^{tree}
# 100644 blob 3f9a01... notes.txt
Tree — це таблиця: права доступу, тип, хеш, ім'я. Рядки бувають двох видів: blob (файл) і tree (вкладена папка). Тобто дерево посилається на інші дерева — і так рекурсивно описується вся структура проєкту. Один tree верхнього рівня = повний знімок проєкту в конкретний момент.
Об'єкт №3: Commit — знімок плюс контекст
Тепер головне. Подивимось на сам коміт:
git cat-file -p HEAD
# tree 841622...
# author Oleksii <o@example.com> 1756339200 +0300
# committer Oleksii <o@example.com> 1756339200 +0300
#
# початок
Ось і весь "коміт" — крихітний текстовий файл з чотирьох полів:
- tree — посилання на знімок проєкту
- parent — посилання на попередній коміт (у першого коміту його немає)
- author/committer — хто і коли
- повідомлення
Жодних "диффів"! Це найпоширеніший міф про Git: буцімто коміт зберігає "зміни". Ні — кожен коміт посилається на повний знімок усього проєкту. А диф, який ти бачиш у git diff чи на GitHub, обчислюється на льоту порівнянням двох знімків.
Ні, і ось чому: файли, що не змінилися, мають той самий вміст → той самий хеш → tree просто посилається на СТАРИЙ blob. Новий об'єкт створюється лише для зміненого. А під час git push або git gc об'єкти додатково пакуються у packfile, де схожі версії зберігаються як дельти. Знімкова модель + дедуплікація за хешем = і швидко, і компактно.
Ланцюжок commit → parent → parent → ... — це і є твоя історія. git log просто йде по ньому, як по зв'язаному списку.
Гілка — це файл на 41 байт. Серйозно
А тепер найкрасивіше. Що таке гілка main? Дивись:
cat .git/refs/heads/main
# a1b2c3d4e5f6... (40 символів хеша + перенос рядка)
Гілка — це текстовий файл з одним хешем коміту. 41 байт. Це все. Ніяких "копій коду", ніяких окремих сховищ — просто закладка, що вказує на коміт.
Тому в Git:
git branch nova-fitcha— миттєва операція: створити файл на 41 байтgit commit— дописати новий коміт і пересунути закладку поточної гілки на ньогоgit checkout main— перемкнути файлHEAD(там записано, яка гілка активна) і розпакувати відповідний знімок у робочу папку
У SVN — попереднику Git — створення гілки означало копіювання файлів і могло тривати хвилини. Лінус зробив гілки безкоштовними — і саме тому весь сучасний workflow (гілка на кожну фічу, pull request'и) став можливим. Дешеві гілки — не фіча Git, а наслідок його структури даних.
Складаємо все докупи
Повна картина того, що відбувається при git commit:
робоча папка --git add--> індекс (staging)
|
git commit
↓
1. створити blob-и для нових/змінених файлів
2. створити tree зі знімком проєкту
3. створити commit (tree + parent + автор + меседж)
4. пересунути refs/heads/поточна-гілка на новий коміт
І тепер розшифровуються всі "магічні" команди:
| Команда | Що робить насправді |
|---|---|
git log | йде по ланцюжку parent-посилань |
git diff A B | порівнює два tree-знімки |
git branch X | створює файл із 41 байта |
git checkout X | міняє HEAD + розпаковує знімок |
git merge | створює коміт із двома parent |
git reset --hard A | пересуває файл гілки на хеш A |
git push | пересилає об'єкти, яких немає на сервері |
До речі, тепер зрозуміло, чому git staged vs unstaged поводяться по-різному: індекс — це чернетка майбутнього tree, яку git add наповнює по одному файлу.
Навіщо це знати, якщо "все і так працює"
Три практичні причини:
- Страх зникає. "Я щось зламав у Git" перестає лякати, коли знаєш: об'єкти незнищенні (навіть після reset вони лежать в
objects/ще тижнями —git reflogїх знайде). Втратити закомічене в Git майже неможливо. - Команди стають передбачуваними. Ти більше не вгадуєш, що зробить
reset,rebaseчиcherry-pick— ти знаєш, які об'єкти й закладки вони чіпають. - Співбесіди. "Що таке гілка в Git?" — питання-фільтр: 9 з 10 кандидатів кажуть "копія коду". Відповідь "вказівник на коміт, файл у refs/heads" — миттєвий сигнал глибини.
Найглибше і безкоштовно — розділ Git Internals книги Pro Git (є українською!). Термін звідти ж: "porcelain" (фарфор) — команди для людей, "plumbing" (сантехніка) — низькорівневі типу cat-file. Цікаве з сучасності: Git поступово мігрує з SHA-1 на SHA-256, а GitHub з 2017 захищається від колізій SHA-1 детектором зловмисних об'єктів.
Часті питання
Що зберігається в папці .git?+
Git зберігає зміни (диффи) чи повні копії файлів?+
Що таке гілка в Git насправді?+
Що таке blob, tree і commit у Git?+
Чи можна втратити код у Git?+
"Тупий трекер вмісту" виявився однією з найелегантніших структур даних у пресі програмного забезпечення: три типи об'єктів, хеш-адресація і файлики-закладки — на цьому тримається робота мільйонів розробників щодня.
Хочеш закріпити базу? Уроки Git — основи та GitHub і віддалені репозиторії, а поруч тримай шпаргалку Git-команд — тепер ти знаєш, що кожна з них робить під капотом.