Вивчай
· 7 хв читання
gitінструменти

Git під капотом — що насправді лежить у папці .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. Зверни увагу на дві речі:

  1. Імені файлу в blob немає. Git зберігає лише вміст. Два файли з однаковим вмістом — один blob.
  2. Хеш обчислюється з вмісту. Зміниш один символ — отримаєш зовсім інший хеш, тобто зовсім інший об'єкт.

Друге — фундаментально: у 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 наповнює по одному файлу.

Навіщо це знати, якщо "все і так працює"

Три практичні причини:

  1. Страх зникає. "Я щось зламав у Git" перестає лякати, коли знаєш: об'єкти незнищенні (навіть після reset вони лежать в objects/ ще тижнями — git reflog їх знайде). Втратити закомічене в Git майже неможливо.
  2. Команди стають передбачуваними. Ти більше не вгадуєш, що зробить reset, rebase чи cherry-pick — ти знаєш, які об'єкти й закладки вони чіпають.
  3. Співбесіди. "Що таке гілка в Git?" — питання-фільтр: 9 з 10 кандидатів кажуть "копія коду". Відповідь "вказівник на коміт, файл у refs/heads" — миттєвий сигнал глибини.
Інфо

Найглибше і безкоштовно — розділ Git Internals книги Pro Git (є українською!). Термін звідти ж: "porcelain" (фарфор) — команди для людей, "plumbing" (сантехніка) — низькорівневі типу cat-file. Цікаве з сучасності: Git поступово мігрує з SHA-1 на SHA-256, а GitHub з 2017 захищається від колізій SHA-1 детектором зловмисних об'єктів.

Часті питання

Що зберігається в папці .git?+
Вся історія проєкту: база об’єктів (objects/ — вміст файлів, знімки папок і коміти, адресовані SHA-1 хешами), закладки-гілки (refs/), вказівник на активну гілку (HEAD) і налаштування (config). Видалиш .git — залишиться просто папка з файлами без історії.
Git зберігає зміни (диффи) чи повні копії файлів?+
Повні знімки, не диффи. Кожен коміт посилається на tree — повний знімок проєкту. Незмінені файли не дублюються: вони мають той самий хеш, тому нове дерево посилається на старі blob-об’єкти. Диф, який показує git diff, обчислюється на льоту.
Що таке гілка в Git насправді?+
Текстовий файл на 41 байт у .git/refs/heads/ з хешем коміту всередині. Гілка — це рухома закладка на коміт, а не копія коду. Саме тому створення гілки миттєве і безкоштовне.
Що таке blob, tree і commit у Git?+
Три типи об’єктів бази Git. Blob — вміст файлу (без імені). Tree — знімок папки: таблиця з імен та хешів blob-ів і вкладених tree. Commit — посилання на tree плюс parent-коміт, автор, дата і повідомлення. Всі об’єкти адресуються хешем свого вмісту.
Чи можна втратити код у Git?+
Закомічений — майже неможливо. Об’єкти в Git незмінні й не видаляються одразу навіть після git reset: git reflog зберігає журнал переміщень HEAD близько 90 днів, тому ’втрачений’ коміт майже завжди можна повернути. Реально втратити можна лише те, що ніколи не комітилось.

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

Хочеш закріпити базу? Уроки Git — основи та GitHub і віддалені репозиторії, а поруч тримай шпаргалку Git-команд — тепер ти знаєш, що кожна з них робить під капотом.