Git репозиторий, где находятся ваши файлы?
Привет! Я разговаривала с другом сегодня о том, как работает Git, и мы затронули тему – где Git хранит ваши файлы? Мы знаем, что они находятся в директории .git, но где именно в ней находятся все версии ваших старых файлов?
Например, этот блог находится в репозитории Git и содержит файл с именем content/post/2019-06-28-brag-doc.markdown
. Где он находится в моей директории .git? И где находятся старые версии этого файла? Давайте исследуем это, написав несколько очень коротких программ на Python.
git хранит файлы в .git/objects
Каждая предыдущая версия каждого файла в вашем репозитории находится в директории .git/objects
. Например, для этого блога в .git/objects
находится 2700 файлов.
$ find .git/objects/ -type f | wc -l
2761
Примечание: .git/objects
на самом деле содержит больше информации, чем "каждая предыдущая версия каждого файла в вашем репозитории", но пока мы не будем вдаваться в эти детали.
Вот очень короткая программа на Python (find-git-object.py
), которая определяет, где хранится любой заданный файл в директории .git/objects.
import hashlib
import sys
def object_path(content):
header = f"blob {len(content)}\0"
data = header.encode() + content
digest = hashlib.sha1(data).hexdigest()
return f".git/objects/{digest[:2]}/{digest[2:]}"
with open(sys.argv[1], "rb") as f:
print(object_path(f.read()))
Эта программа выполняет следующие действия:
- Считывает содержимое файла.
- Вычисляет заголовок (blob 16673\0) и объединяет его с содержимым файла.
- Вычисляет хеш-сумму SHA-1 (например, e33121a9af82dd99d6d706d037204251d41d54 в данном случае).
- Переводит эту хеш-сумму в путь (.git/objects/e3/3121a9af82dd99d6d706d037204251d41d54).
Мы можем запустить ее:
$ python3 find-git-object.py content/post/2019-06-28-brag-doc.markdown
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
Термин для этой стратегии хранения данных (где имя файла объекта в базе данных совпадает с хешем содержимого файла) называется "хранилищем с адресацией по содержимому" (content addressed storage).
Одной из интересных особенностей хранилища с адресацией по содержимому является то, что если у меня есть два файла (или 50 файлов!) с точно таким же содержанием, это не занимает дополнительное место в базе данных Git – если хеш содержания равен aabbbbbbbbbbbbbbbbbbbbbbbbb, оба файла будут сохранены в .git/objects/aa/bbbbbbbbbbbbbbbbbbbbb.
Как кодируются эти объекты?
Если я попробую посмотреть этот файл в .git/objects
, то это будет выглядеть немного странно:
$ cat .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
x^A<8D><9B>}s<E3>Ƒ<C6><EF>o|<8A>^Q<9D><EC>ju<92><E8><DD>\<9C><9C>*<89>j<FD>^...
Давайте попробуем разобраться:
$ file .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54: zlib compressed data
Просто сжатие данных! Мы можем написать еще одну небольшую программу на Python, называемую decompress.py
, которая использует модуль zlib для распаковки данных:
import zlib
import sys
with open(sys.argv[1], "rb") as f:
content = f.read()
print(zlib.decompress(content).decode())
Давайте теперь распакуем:
$ python3 decompress.py .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
blob 16673---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... the entire blog post ...
Данные кодируются довольно просто: есть строка "blob 16673\0", а затем полное содержание файла.
Здесь нет диффов
Одна вещь, которая удивила меня здесь, когда я впервые об этом узнала: здесь нет никаких диффов! Этот файл представляет собой девятую версию этой статьи в блоге, но версия, которую Git хранит в .git/objects
, представляет собой весь файл, а не разницу с предыдущей версией.
На самом деле Git иногда также хранит файлы в виде диффов (когда вы выполняете git gc
, он может объединять несколько разных файлов в "пак-файл" для повышения эффективности), но мне никогда не приходилось об этом думать в своей жизни, поэтому мы не будем вдаваться в это. Адитья Мукерджи написал отличную статью под названием "Unpacking Git packfiles" о том, как работает этот формат.
Как на счет старых версий этого поста?
Теперь вы могли бы задаться вопросом: если есть 8 предыдущих версий этой статьи в блоге (до того, как я исправила некоторые опечатки), где они находятся в директории .git/objects
? Как их найти?
Сначала давайте найдем каждый коммит, в котором этот файл изменился, с помощью команды git log:
$ git log --oneline content/post/2019-06-28-brag-doc.markdown
c6d4db2d
423cd76a
7e91d7d0
f105905a
b6d23643
998a46dd
67a26b04
d9999f17
026c0f52
72442b67
Теперь давайте выберем предыдущий коммит, скажем, 026c0f52
. Коммиты также хранятся в .git/objects
, и мы можем попробовать посмотреть на него там. Но коммита там нет! Команда ls .git/objects/02/6c*
не возвращает результатов! Вы помните, как мы упомянули "иногда Git упаковывает объекты, чтобы экономить место, но нам не нужно беспокоиться об этом"? Думаю, сейчас пришло время разобраться с этим.
Итак, давайте разбираться.
Распакуем несколько объектов
Нам нужно распаковать объекты из pack-файлов. Я подсмотрела это на Stack Overflow, и, кажется, можно сделать это следующим образом:
$ mv .git/objects/pack/pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack .
$ git unpack-objects < pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack
Это действительно странная операция с репозиторием, поэтому она может вызвать некоторую тревогу, но всегда можно просто снова склонировать репозиторий с GitHub, если что-то пойдет не так, поэтому я особо не беспокоилась.
После распаковки всех объектных файлов у нас оказалось гораздо больше объектов: около 20 000 вместо примерно 2700. Это интересно.
find .git/objects/ -type f | wc -l
20138
Вернемся к коммиту
Теперь мы можем вернуться к просмотру нашего коммита 026c0f52
. Вы помните, что мы сказали, что не все в .git/objects
- это файлы? Некоторые из них являются коммитами! И чтобы выяснить, где хранится старая версия нашего файла content/post/2019-06-28-brag-doc.markdown
, нам нужно довольно глубоко копнуть в этот коммит.
Первый шаг - посмотреть на коммит в .git/objects
.
Коммит, шаг 1: смотрим на коммит
Коммит 026c0f52
теперь находится в .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
после выполнения некоторых действий по распаковке, и мы можем посмотреть на него так:
$ python3 decompress.py .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
commit 211tree 01832a9109ab738dac78ee4e95024c74b9b71c27
parent 72442b67590ae1fcbfe05883a351d822454e3826
author Julia Evans <julia@jvns.ca> 1561998673 -0400
committer Julia Evans <julia@jvns.ca> 1561998673 -0400
brag doc
Мы также можем получить ту же информацию с помощью команды git cat-file -p 026c0f52
, которая делает то же самое, но более аккуратно форматирует данные. (опция -p означает "красивое форматирование, пожалуйста")
Коммит, шаг 2: смотрим не дерево
Этот коммит имеет дерево (tree). Что это такое? Давайте посмотрим. Идентификатор (ID) дерева - 01832a9109ab738dac78ee4e95024c74b9b71c27, и мы можем использовать наш скрипт decompress.py
, который мы использовали ранее, чтобы посмотреть этот git-объект (хотя мне пришлось удалить .decode(), чтобы скрипт не выдавал ошибку).
$ python3 decompress.py .git/objects/01/832a9109ab738dac78ee4e95024c74b9b71c27
b'tree 396\x00100644 .gitignore\x00\xc3\xf7`$8\x9b\x8dO\x19/\x18\xb7}|\xc7\xce\x8e:h\xad100644 README.md\x00~\xba\xec\xb3\x11\xa0^\x1c\xa9\xa4?\x1e\xb9\x0f\x1cfG\x96\x0b
Этот текст отформатирован в довольно непригодном для чтения виде. Основная проблема отображения здесь заключается в том, что хеши коммитов (\xc3\xf7$8\x9b\x8dO\x19/\x18\xb7}|\xc7\xce\…
) представлены в виде сырых байтов, а не в шестнадцатеричной системе. Поэтому мы видим \xc3\xf7$8\x9b\x8d
вместо c3f76024389b8d
. Давайте переключимся на использование команды git cat-file -p
, которая форматирует данные более дружелюбным образом, потому что мне не хочется писать парсер для этого.
$ git cat-file -p 01832a9109ab738dac78ee4e95024c74b9b71c27
100644 blob c3f76024389b8d4f192f18b77d7cc7ce8e3a68ad .gitignore
100644 blob 7ebaecb311a05e1ca9a43f1eb90f1c6647960bc1 README.md
100644 blob 0f21dc9bf1a73afc89634bac586271384e24b2c9 Rakefile
100644 blob 00b9d54abd71119737d33ee5d29d81ebdcea5a37 config.yaml
040000 tree 61ad34108a327a163cdd66fa1a86342dcef4518e content <-- сюда мы пойдем дальше
040000 tree 6d8543e9eeba67748ded7b5f88b781016200db6f layouts
100644 blob 22a321a88157293c81e4ddcfef4844c6c698c26f mystery.rb
040000 tree 8157dc84a37fca4cb13e1257f37a7dd35cfe391e scripts
040000 tree 84fe9c4cb9cef83e78e90a7fbf33a9a799d7be60 static
040000 tree 34fd3aa2625ba784bced4a95db6154806ae1d9ee themes
Это показывает все файлы, которые у меня были в корневом каталоге репозитория на момент этого коммита. Похоже, что я случайно зафиксировала какой-то файл с именем mystery.rb
в какой-то момент, который позже был удален.
Наш файл находится в каталоге content
, поэтому давайте посмотрим на это дерево: 61ad34108a327a163cdd66fa1a86342dcef4518e
.
Коммит, шаг 3: еще одно дерево
$ git cat-file -p 61ad34108a327a163cdd66fa1a86342dcef4518e
040000 tree 1168078878f9d500ea4e7462a9cd29cbdf4f9a56 about
100644 blob e06d03f28d58982a5b8282a61c4d3cd5ca793005 newsletter.markdown
040000 tree 1f94b8103ca9b6714614614ed79254feb1d9676c post <-- сюда мы пойдем дальше!
100644 blob 2d7d22581e64ef9077455d834d18c209a8f05302 profiler-project.markdown
040000 tree 06bd3cee1ed46cf403d9d5a201232af5697527bb projects
040000 tree 65e9357973f0cc60bedaa511489a9c2eeab73c29 talks
040000 tree 8a9d561d536b955209def58f5255fc7fe9523efd zines
Все еще не конец.
Коммит, шаг 4: и еще одного дерево
Файл, который мы ищем, назодится в каталоге post
, поэтому смотрим еще одно дерево:
$ git cat-file -p 1f94b8103ca9b6714614614ed79254feb1d9676c
.... MANY MANY lines omitted ...
100644 blob 170da7b0e607c4fd6fb4e921d76307397ab89c1e 2019-02-17-organizing-this-blog-into-categories.markdown
100644 blob 7d4f27e9804e3dc80ab3a3912b4f1c890c4d2432 2019-03-15-new-zine--bite-size-networking-.markdown
100644 blob 0d1b9fbc7896e47da6166e9386347f9ff58856aa 2019-03-26-what-are-monoidal-categories.markdown
100644 blob d6949755c3dadbc6fcbdd20cc0d919809d754e56 2019-06-23-a-few-debugging-resources.markdown
100644 blob 3105bdd067f7db16436d2ea85463755c8a772046 2019-06-28-brag-doc.markdown <-- вот он!!!!!
Здесь файл 2019-06-28-brag-doc.markdown
указан последним в списке, потому что это была самая последняя статья в блоге на момент публикации.
Коммит, шаг 5: мы это сделали!
Наконец, мы нашли объектный файл, где находится предыдущая версия моей статьи в блоге! Ура! Ему присвоена хеш-сумма 3105bdd067f7db16436d2ea85463755c8a772046
, поэтому он находится в .git/objects/31/05bdd067f7db16436d2ea85463755c8a772046
.
Мы можем посмотреть на него с помощью decompress.py
:
$ python3 decompress.py .git/objects/31/05bdd067f7db16436d2ea85463755c8a772046 | head
blob 15924---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... rest of the contents of the file here ...
Это старая версия статьи! Если бы я выполнила команду git checkout 026c0f52 content/post/2019-06-28-brag-doc.markdown
или git restore --source 026c0f52 content/post/2019-06-28-brag-doc.markdown
, я получила бы именно это содержание.
Этот процесс обхода дерева является тем, как работает команда git log
Весь этот процесс, который мы только что прошли (нахождение коммита, проход по различным деревьям каталогов, поиск нужного имени файла), может показаться долгим и сложным, но на самом деле это происходит за кулисами, когда мы выполняем команду git log content/post/2019-06-28-brag-doc.markdown
. Она должна пройти через каждый коммит в вашей истории, проверить версию (например, 3105bdd067f7db16436d2ea85463755c8a772046
в данном случае) файла content/post/2019-06-28-brag-doc.markdown
и узнать, изменился ли файл с предыдущего коммита.
Именно поэтому команда git log ИМЯ_ФАЙЛА
иногда работает немного медленно - в моем репозитории 3000 коммитов, и для каждого коммита ей нужно выполнить много работы, чтобы выяснить, изменился ли файл в этом коммите или нет.
Сколько у меня есть предыдущих версий файлов?
Прямо сейчас у меня есть 1530 отслеживаемых файлов в моем репозитории:
$ git ls-files | wc -l
1530
Сколько исторических файлов всего есть? Мы можем перечислить все в .git/objects
, чтобы узнать, сколько объектных файлов там находится:
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | wc -l
20135
Однако не все из них представляют собой предыдущие версии файлов - как мы видели ранее, многие из них являются коммитами и деревьями каталогов. Но мы можем написать еще одну небольшую программу на Python, называемую find-blobs.py, которая просматривает все объекты и проверяет, начинаются ли они с "blob" или нет:
import zlib
import sys
for line in sys.stdin:
line = line.strip()
filename = f".git/objects/{line[0:2]}/{line[2:]}"
with open(filename, "rb") as f:
contents = zlib.decompress(f.read())
if contents.startswith(b"blob"):
print(line)
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | python3 find-blobs.py | wc -l
6713
Похоже, в моем репозитории Git осталось 6713 - 1530 = 5183 старых версий файлов, которые Git сохраняет для меня на всякий случай, если мне когда-нибудь понадобится вернуться к ним. Это довольно удобно!
Вот и все!
Вот ссылка на все коды из этой публикации. Их не так уж и много.
Я думала, что знаю, как работает Git, но до этого момента я никогда не задумывалась о pack-файлах, поэтому это было интересным исследованием. Также я не уделяла слишком много времени размышлениям о том, сколько работы фактически выполняет git log, когда я прошу его отслеживать историю файла, поэтому было интересно вдуматься в это.
Как забавное дополнение: как только я зафиксировала эту статью в блоге, Git начал ругаться на количество объектов в моем репозитории (я предполагаю, что 20 000 слишком много!), и запустила git gc
для их сжатия в pack-файлы. Теперь моя директория .git/objects
стала очень маленькой:
$ find .git/objects/ -type f | wc -l
14