Работа с памятью при использовании Git

Ярослав Ильяшенко

Постараюсь быть максимально кратким. На сайте русской документации по Git дается такое объяснение принципу его работы:

Git считает хранимые данные набором слепков небольшой файловой системы. Каждый раз, когда вы фиксируете текущую версию проекта, Git, по сути, сохраняет слепок того, как выглядят все файлы проекта на текущий момент. Ради эффективности, если файл не менялся, Git не сохраняет файл снова, а делает ссылку на ранее сохранённый файл.

Так вот, если очень грубо допустить, что размер текстового файла стремится к 1Мб, то следуя логике работы Git при даже самом малом изменении в этом файле(исправили ошибку в слове), будет создаваться "слепок"(=копия) файла, размером ~1Мб.

Вопрос: насколько это выгодно с точки зрения используемой памяти, или же текстовой файл как-то делится на более мелкие файлы, и созданию "слепков" подлежат уже они?

4 ответа

Ярослав Ильяшенко

Кратко: приведенная схема - это объяснение наблюдаемого поведения GIT. Если вы будете работать с GIT стандартными командами и не будете лезть руками в бинарные файлы - всё будет выглядеть как будто все так и работает. Но это вовсе не означает что на каждую версию файла будет и правда тратиться по мегабайту...

Потому что на более низком уровне, гит все-таки использует дельты между файлами. Прочитать про них можно вот тут: Git изнутри - Pack-файлы

Однако, упаковка файлов происходит не всегда, а только при достижении некоторого порога числа распакованных объектов или при пушу коммитов в удаленный репозиторий. При наличии больших файлов в некоторых случаях приходится явно выполнять команду git gc для переупаковки всех блобов (нам, к примеру, однажды пришлось добавлять эту команду в билд-скрипт, потому что на билд-сервере отправка в удаленные репозитории никогда не происходит и локальный репозиторий бесконтрольно рос).


Ярослав Ильяшенко

Как раз об этом повествует "Git изнутри - Pack-файлы". Собственно, там и ответ на вопрос:

Однако, время от времени Git упаковывает несколько таких объектов в один pack-файл (pack в пер. с англ. — упаковывать, уплотнять) для сохранения места на диске и повышения эффективности. Это происходит, когда "рыхлых" объектов становится слишком много, а также при вызове git gc вручную, и при отправке изменений на удалённый сервер.

То есть, через какое-то время в процессе эксплуатации репозитория различающиеся версии файла попадут в один Pack-файл, который представляет разные версии гораздо более эффективным образом: через основу и дельты (разности, изменения).


Ярослав Ильяшенко

или же текстовой файл как-то делится на более мелкие файлы, и созданию "слепков" подлежат уже они?

Нет, файлы (как и любой другой объект, например, папка — список пар «имя дочернего объекта — его хэш») сохраняются именно целиком. Это, кстати, повышает «выживаемость» репозитория — если часть объектов по каким-то причинам будет повреждена или стёрта, остаётся крайне высокая вероятность восстановления если и не последнего commit-а, то хотя бы одного из последних.

Однако параллельно с объектным хранилищем (./git/objects) есть и сжатое, где все объекты хранятся в одном упакованном файле (.git/objects/pack/) в виде diff-ов. Туда можно поместить любой объект, при этом оригинальный файл объекта удаляется.

Так как операция упаковки является достаточно трудоёмкой, она выполняется только при вызове команды git gc --force. При этом любые дальнейшие изменения в репозитории (добавление commit-ов и переписывание истории) продолжат добавление объектов в основное хранилище; во втором же случае в pack-файле также останутся «хвосты», которые будут удалены только при следующем вызове git gc.


Ярослав Ильяшенко

TL;DR Файлы в гит хранятся целиком и не делятся. Это достаточно выгодно.

Есть хорошие статьи по git (пример, пример), где раскрывается внутреннее устройство гит.

Пользователь запускает git add на data/letter.txt. Происходят две вещи.

Во-первых, создаётся новый блоб-файл в директории .git/objects/. Он содержит сжатое содержимое data/letter.txt. Его имя – сгенерированный хэш на основе содержимого. К примеру, Git делает хэш от а и получает 2***************************************. Первые два символа хэша используются для имени директории в базе объектов: .git/objects/2e/. Остаток хэша – это имя блоб-файла, содержащего внутренности добавленного файла: .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

Заметьте, что простое добавление файла в Git приводит к сохранению его содержимого в директории objects. Оно будет храниться там, если пользователь удалит data/letter.txt из рабочей копии.

Далее показывается механика того, что происходит когда файл меняется.

Когда пользователь создал data/number.txt, он хотел написать 1, а не 1234. Он вносит изменение и снова добавляет файл к индексу. Эта команда создаёт новый блоб с новым содержимым.

Можете посмотреть файлы в папке object и через команду git cat-file -p увидеть разжатое представление ваших файлов.

Это механика на уровне отдельных файлов. Казалось бы - неоптимальный расход. Тем не менее, есть дополнительный механизм - когда происходит упаковка объектов в более крупные файлы.

Проблема с хранением больших файлов также привела к появлению LFS. Дело в том, что большие файлы, которые вы даже ни разу не правили (бинарники типа картинок) сохранять в git реально достаточно тяжело, поэтому их сохраняют на отдельные файловые хранилища.

licensed under cc by-sa 3.0 with attribution.