Систему контроля версий Git первый раз довелось использовать в 2011 году.
Это был достаточно “сомнительный” переход с SVN, потому что еще до старта проекта горели все сроки, лид разработки решил использовать новый фреймворк, но через неделю был переведен в другую команду.
В качестве СУБД “прилетел” неведанный Firebird с ворохом хранимых процедур, потому что мы создавали web-канал взаимодействия для существующего desktop решения.
Меня торжественно нарекли новым лидом над тремя младшими специалистами, невзирая на обстоятельство, что я был четвертым участником аналогичной позиции.
За последние несколько лет моей практики альтернативы Git перестали встречаться окончательно. Единственное, что остается неизменным - это младшие специалисты и регулярные ситуации “сломалось/потерялось” и “забыли/перепутали”.
Данная статья написана по мотивам официальной документации Git и личного опыта. В примерах команд используется Git версии 2.43 в виде консольного клиента Git Bash. В большинстве утверждений предполагается работа со стандартной конфигурацией, без отдельных оговорок про различные возможности кастомизации через настройки, параметры команд и переменные окружения.
Git - это распределенная система контроля версий. Технически здесь нет деления на клиента и сервер, а все участники могут существовать независимо, при необходимости равноправно обмениваясь изменениями напрямую между собой. Но с целью снижения количества связей и формализации “источника правды”, обычно выделяют сервер, с которым уже работают остальные пользователи. Репозитории участников принято называть локальными, а репозиторий на сервере - удаленным.
Что не так с деревьями
В предыдущей статье были рассмотрены все 4 типа объектов внутреннего хранилища Git. Их содержимое преимущественно представляет собой текстовый файл, к которому применяется сжатие по алгоритму zlib, как это было показано на примере blob.
Такой подход позволяет без особых сложностей создавать различные утилиты для взаимодействия с внутренним хранилищем репозитория, даже в обход уже существующих сантехнических команд. Но давайте попробуем повторить эти манипуляции с объектом tree, который был сформирован при создании второго коммита.
Его полный ключ: 2e91f5b82b6fdef912a1f2c00eac01cec57452bc
1akorolev.dev content (content-article-01)
2git cat-file -t 2e91f5b82b6fdef912a1f2c00eac01cec57452bc
3tree
4
5akorolev.dev content (content-article-01)
6git cat-file -s 2e91f5b82b6fdef912a1f2c00eac01cec57452bc
771
8
9akorolev.dev content (content-article-01)
10openssl zlib -d -in .git/objects/2e/91f5b82b6fdef912a1f2c00eac01cec57452bc
11tree 71100644 LICENSE▒▒#▒p▒`▒▒?▒▒2▒4▒z100644 main.txt▒⛲▒▒CK▒)▒wZ▒▒▒S▒
12akorolev.dev content (content-article-01)
13git cat-file tree 2e91f5b82b6fdef912a1f2c00eac01cec57452bc
14100644 LICENSE▒▒#▒p▒`▒▒?▒▒2▒4▒z100644 main.txt▒⛲▒▒CK▒)▒wZ▒▒▒S▒
Наблюдаем, что это объект нужного нам типа, и его основное содержимое состоит из 71 байта информации. Но вот попытка получить сами данные не выглядит успешной. При этом “поломанные символы” отображаются, как в случае прямой распаковки, так и при использовании знакомой команды cat-file.
Типы файлов
Еще на уроках информатики из беззаботных школьных времен или первых курсов университета, принято говорить о делении файлов на бинарные и текстовые. С точки же зрения принципов работы вычислительной техники все файлы являются бинарными, а существующее деление лишь сказывается на способе обработки этих нулей и единиц.
При обработке текстовых файлов оперируют строками, отдельными символами и их кодировкой. А кодировка как раз и определяет сколько и каких именно байт необходимо записать для каждого отдельного символа.
Например, Java с 9 версии хранит строки в виде массива байт и в зависимости от ситуации использует LATIN-1 или UTF-16. Git в качестве многобайтной кодировки оперирует UTF-8 , в которой отдельный символ может потребовать от 1 до 4 байт информации.
“Нестабильный” вес объектов типа tree
Полученный ранее вывод в консоль - это не последствие какой-либо ошибки, а всего лишь особенность хранения содержимого. Если команду cat-file использовать с параметром -p, то вывод немного изменится.
1akorolev.dev content (content-article-01)
2git cat-file -p 2e91f5b82b6fdef912a1f2c00eac01cec57452bc
3100644 blob 8b912315c970c8609f953ff98d32aa123415e97a LICENSE
4100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 main.txt
Здесь можем наблюдать два заметных отличия:
- Все символы начинают отображаться корректно и понятно для человека
- Данных выводится заметно больше, чем ранее
1akorolev.dev content (content-article-01)
2openssl zlib -d -in .git/objects/2e/91f5b82b6fdef912a1f2c00eac01cec57452bc | wc --bytes
379
4
5akorolev.dev content (content-article-01)
6git cat-file tree 2e91f5b82b6fdef912a1f2c00eac01cec57452bc | wc --bytes
771
8
9akorolev.dev content (content-article-01)
10git cat-file -p 2e91f5b82b6fdef912a1f2c00eac01cec57452bc | wc --bytes
11123
Первые значения в 79 и 71 байт вполне объяснимы:
71 (основное содержимое объекта) + 4 (tree) + 1 (символ пробела) + 2 (само число 71 в метаинформации объекта) + 1 ( NUL символ) = 79 (полное содержимое объекта до сжатия)
Но вот понятный нам вывод содержит на 52 байта больше информации, что составляет 73%. Может по какой-то причине хранение данных для объекта типа tree реализовано несколькими файлами? Или у вас возникли какие-то иные предположения за счет чего это происходит? Обязательно поделитесь ими в комментариях.
Гибридное устройство объектов типа tree
До этого мы анализировали содержимое объекта только в текстовом представлении. Самое время взглянуть на него в формате более низкого уровня.
Это полезный навык для IT-специалистов, например, для поиска непечатаемых символов. Из недавнего вспоминается ошибка валидации xml, потому что в сообщении kafka присылали BOM перед xml-прологом.
Мы не будем спускаться до уровня нолей и единиц, а воспользуется HEX-редактором, т.е. представлением в шестнадцатеричном виде.
1akorolev.dev content (content-article-01)
2git cat-file tree 2e91f5b82b6fdef912a1f2c00eac01cec57452bc | xxd -d
300000000: 3130 3036 3434 204c 4943 454e 5345 008b 100644 LICENSE..
400000016: 9123 15c9 70c8 609f 953f f98d 32aa 1234 .#..p.`..?..2..4
500000032: 15e9 7a31 3030 3634 3420 6d61 696e 2e74 ..z100644 main.t
600000048: 7874 00e6 9de2 9bb2 d1d6 434b 8b29 ae77 xt........CK.).w
700000064: 5ad8 c2e4 8c53 91 Z....S.
Первый 15 байт нам уже встречались. Это метка, которая обозначает простой файл, далее символ пробела, наименование файла и NUL. Далее идут 20 непонятных символов, после которых опять метка 100644 и название второго файла. Заканчивается вывод еще 20 странными символами. Если же сопоставить последовательность каждых 20 символов с их шестнадцатеричными кодами, то можно заметить следующее:
- ..#..p.`..?..2..4..z 8b912315c970c8609f953ff98d32aa123415e97a
- …….CK.).wZ….S. e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
То есть значения справа соответствуют ключам от объектов типа blob для файлов LICENSE и main.txt
В предыдущей статье указывалось, что по алгоритму SHA-1 мы получаем сорока символьный хеш. Так вот для сохранения отдельного “текстового” символа используется минимум один байт информации, а для шестнадцатеричной цифры хватит всего 4 бита, т.е. в два раза меньше.
Это дает экономию в 20 байт для ключа каждого дочернего элемента дерева. С учетом количества файлов и директорий в реальных проектах суммарно формируется значительный объем информации. Но одновременно это же является причиной, что объекты типа tree представляют собой до сжатия некий гибрид представления данных.
Поиск оставшихся 12 байт
При сравнении количества информации между разными способами вывода содержимого у нас была разница в 52 байта. Мы уже раскрыли секрет экономии 40 байт. Осталось найти всего 12, только вот “лишних” данных от первого вывода не осталось. Тут все более банально, просто параметр -p добавляет данные, которые нужны сугубо для удобства восприятия информации пользователем.
1akorolev.dev content (content-article-01)
2git cat-file -p 2e91f5b82b6fdef912a1f2c00eac01cec57452bc | xxd -d
300000000: 3130 3036 3434 2062 6c6f 6220 3862 3931 100644 blob 8b91
400000016: 3233 3135 6339 3730 6338 3630 3966 3935 2315c970c8609f95
500000032: 3366 6639 3864 3332 6161 3132 3334 3135 3ff98d32aa123415
600000048: 6539 3761 094c 4943 454e 5345 0a31 3030 e97a.LICENSE.100
700000064: 3634 3420 626c 6f62 2065 3639 6465 3239 644 blob e69de29
800000080: 6262 3264 3164 3634 3334 6238 6232 3961 bb2d1d6434b8b29a
900000096: 6537 3735 6164 3863 3265 3438 6335 3339 e775ad8c2e48c539
1000000112: 3109 6d61 696e 2e74 7874 0a 1.main.txt.
Для каждой из двух записей такой набор состоит из:
- blob - HEX: 62 6c6f 62; 4 байта - тип дочернего объекта, который однозначно определяется по метке прав доступа 100644
- пробел после типа дочернего объекта - HEX: 20; 1 байт
- символ горизонтальной табуляции перед наименованием дочернего объекта - HEX: 09; 1 байт
- символ перевода строки - HEX: 0a; 1 байт
Да, если по вашим математическим подсчетам (4 + 1 + 1 + 1) * 2 = 14, а не 12, то это также легко объяснить. Просто в данном выводе отсутствует NUL символ после наименования дочернего объекта, который также занимают 1 байт.
Постскриптум
Рассмотренный подход к формированию tree объектов является одним из примеров пользы применения бинарных протоколов. Стоит отметить, что это не единственный механизм Git, который оперирует данными в двоичном формате. Например, есть index, который уже чисто бинарный и использует даже битовые флаги, также существуют бинарные pack-файлы.
Впрочем, это уже совсем другая история…
Источник изображения в заголовке Unsplash. Автор niko photos .