Snapshots vs deltas — как Git хранит данные
Если в одной строке описать, что отличает Git от всех VCS, появившихся до него — это будет: Git хранит snapshots, а не дельты. Каждый коммит — это полный снимок состояния всех файлов проекта на момент коммита, а не разница (diff) от предыдущей версии.
На первый взгляд это звучит расточительно. «Если у нас 10 000 коммитов и проект на 50 МБ, неужели мы храним 10 000 × 50 МБ = полтерабайта?» Нет, не храним. И это связано с другим важным концептом: content-addressable storage. В этом уроке разберём, как Git реально хранит данные, и почему это так важно.
Как раньше: дельты в RCS, SCCS, частично в Subversion
В большинстве VCS до Git модель была такой: хранится одна полная версия файла, а остальные — как diff (дельты) к ней. Например, в RCS:
Плюсы дельт: экономия места. Один файл в 100 версиях с мелкими изменениями занимает в разы меньше, чем 100 полных копий.
Минусы:
- Скорость чтения зависит от длины цепочки. Чтобы получить версию 100, нужно применить 99 patches.
- Дельты сложно мерджить. Если две ветки расходятся, объединение цепочек дельт — сложная задача.
- Целостность хрупкая. Если одна дельта повреждена — все следующие версии нечитаемы.
- Renames и moves плохо работают. Дельты привязаны к файлу. Если файл переименован — теряется связь.
Subversion использовал гибридную модель: хранил полные snapshots каждые N версий и дельты между ними. Это смягчало проблему скорости, но не решало остальных.
Как Git: snapshots всего дерева
Git устроен принципиально иначе. Каждый commit — это полный snapshot дерева файлов на момент коммита. Не diff от предыдущего, а независимый, целостный slice состояния.
Звучит как «100 версий = 100 полных копий, диск взорвётся»? Нет. Тут вступает второй ключевой концепт.
Content-addressable storage: хранилище по содержимому
Git хранит файлы и деревья в content-addressable storage — хранилище, в котором адрес объекта — это его содержимое. А именно — SHA-1 хеш от содержимого.
Если файл не изменился между коммитами — у него тот же SHA-1. А значит — он хранится один раз, и оба коммита ссылаются на тот же blob.
То есть: если в commit 2 изменился только foo.c, мы добавляем один новый blob (новая версия foo.c) и новое tree-объект (которое ссылается на новый foo.c и старые bar.c, README). Остальные blob-ы переиспользованы. Экономия места — фактически такая же, как в дельта-модели.
Но плюсы — гигантские:
- Скорость чтения. Не нужно «применять цепочку дельт». Достал нужный blob по SHA — получил содержимое.
- Целостность. SHA-1 хеш — это математический отпечаток содержимого. Если в blob изменится хоть один бит — SHA изменится. Битая дельта в RCS могла остаться незамеченной. В Git битый blob — это другой SHA, мгновенно детектится.
- Дедупликация автоматическая. Не нужно никаких хитрых алгоритмов. Просто хеш и проверка «есть ли уже такой объект».
- Merge простой. Сравниваешь два tree-объекта, видишь, где SHA blob-ов разные — там и есть изменения. Никакой реконструкции цепочек.
SHA-1: что это вообще такое
SHA-1 — это криптографическая хеш-функция: берёт на вход произвольное содержимое, выдаёт на выход 160-битное (20-байтовое) значение, обычно представляемое как 40 шестнадцатеричных символов.
Пример: содержимое строки «hello world\n» под SHA-1 для blob-объекта Git:
echo "hello world" | git hash-object --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
Это и есть «адрес» этого blob-а в Git. В любом репозитории, в любом мире, содержимое «hello world\n» будет иметь SHA-1 3b18e5.... Это математическое свойство, не настройка Git.
Свойства SHA-1 для Git:
- Детерминированный — одно и то же содержимое всегда даёт один SHA.
- Лавинообразный — изменение даже одного бита даёт радикально другой SHA.
- Невозможность коллизий на практике — придумать другое содержимое с тем же SHA вычислительно очень трудно (но не невозможно — см. ниже).
- Невозможность инвертирования — по SHA нельзя восстановить содержимое.
В Git SHA-1 используется как:
- Адрес объекта в хранилище: путь к blob с SHA
3b18e5...— это.git/objects/3b/18e5...(первые 2 символа = поддиректория). - Identifier коммита:
git logпоказывает SHA коммита. На него ссылаются ветки, теги, parent-указатели. - Гарантия целостности: при чтении объекта Git хеширует его и сравнивает с именем файла. Не совпадает — ошибка.
SHA-1 -> SHA-256: переход в Git 3.0
В 2017 году компания Google и нидерландский CWI Institute опубликовали первую практическую коллизию SHA-1 — два PDF-файла с разным содержимым и одинаковым SHA-1. Атака стоила ~$100k compute времени. Это в принципе означает: SHA-1 криптографически сломан.
Для Git это не критично немедленно (атаки на репозиторий через коллизию пока непрактичны), но коммьюнити начало миграцию. Git 2.13 (2017) добавил защиту от известных коллизий. Git 2.29 (2020) добавил экспериментальную поддержку SHA-256. Git 2.42 (2023) сделал SHA-256 стабильной фичей для новых репозиториев.
На май 2026 (актуальная версия Git 2.54) большинство репозиториев в мире — на SHA-1. GitHub, GitLab — все ещё SHA-1 по умолчанию (поддержка SHA-256 в роадмапе). Когда выйдет Git 3.0, начнётся плавный переход. Junior DE стоит знать про этот переход, но в реальной работе ближайшие 1-2 года вы будете работать с SHA-1 хешами.
Практически: SHA-1 — это 40 символов hex. SHA-256 — это 64 символа hex. Если вы видите 64-символьный «коммит» — это репозиторий на SHA-256. Они несовместимы по протоколу — два репозитория на разных хешах не могут push/pull друг друга. Git готовит interop-формат, но пока его нет.
# Создать репозиторий с SHA-256 (опционально, Git 2.42+):
git init --object-format=sha256
# Проверить, какой формат у текущего репо:
git rev-parse --show-object-format
# sha1 (default)
Immutability: SHA = подпись содержимого
Из content-addressable storage следует immutability — неизменяемость объектов. Если вы изменили blob, у него меняется SHA, и это уже другой объект. Старый остаётся на месте.
Из immutability вытекают важные свойства:
- Параллельная история возможна. Несколько разработчиков могут одновременно расширять историю, не «портя» объекты друг друга.
- Backups простые. Скопировал
.git/objects/— у тебя гарантированно консистентный backup, потому что объекты иммутабельны. - Garbage collection. Когда коммит больше не достижим (ни одна ветка/tag на него не ссылается), Git может его удалить. Но не сразу — есть grace period 30 дней.
- Reflog работает. Даже если вы «удалили» коммит через
git reset --hard, его SHA остаётся в reflog ещё 30 дней. Можно восстановить (модуль 10).
Что мы получили в сухом остатке
В следующем уроке посмотрим, почему именно Git победил конкурентов (Mercurial, Bazaar) и стал индустриальным стандартом. Подсказка: всё это (snapshots, content-addressable storage) — не уникально для Git, но именно Git довёл эту модель до production-готового состояния и получил GitHub в нужный момент.
Колоночное хранение vs построчное: ментальная модельПопробуй сам
Если установлен Git, попробуйте увидеть content-addressable storage руками. Команды будем разбирать подробно в модуле 4, пока просто попробуйте:
mkdir -p ~/git-sandbox/lesson-01-snapshots
cd ~/git-sandbox/lesson-01-snapshots
git init
# Создайте файл
echo "hello world" > hello.txt
# Посмотрите, какой будет SHA у этого blob
git hash-object hello.txt
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# Создайте файл с тем же содержимым с другим именем
echo "hello world" > greeting.txt
git hash-object greeting.txt
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad — ТОЧНО ТО ЖЕ
# Это и есть content-addressable: содержимое определяет адрес
Магия в том, что SHA полностью определён содержимым. Имя файла, дата, владелец — не учитываются. Это и обеспечивает дедупликацию.