Git Objects
Это типа конспект, который я веду для себя, чтобы получше разобраться и запомнить. Вероятно, вам лучше сразу читать оригинал: 10.2 Git Objects.
Также, поскольку оригинальная статья игнорит тему annotated tags,
то внизу есть добавочный раздел про тэги, сделанный на основе статьи Types of git objects из Curious git. UPD: Про тэги написано в следующей главе.
Короткие выводы
Четыре стандартных вида объектов: blob, tree, commmit, tag.
Три режима блобов в дереве:
100644обычный файл,100755исполняемый,120000симлинк.
Любой объект читается командой git cat-file -p <obj-id>
Тип объекта берётся той же командой, только вместо флага -p ставится флаг -t
Эксперименты
Мы можем точно воспроизводить все действия и получать в точности те же самые хэши, что и в мануале. Но лишь до тех пор, пока не дойдём до раздела про коммиты.
Команда git commit-tree берёт системное время, оно каждый раз разное и поэтому
получается каждый раз разный хэш.
Но мы, конечно, хотим в точности воспроизвести мануал и этого раздела тоже.
Поехали!
Накидываем блобы, повторяя команды из мануала:
$ mkdir myrepo && cd myrepo && git init .
Initialized empty Git repository in ~/myrepo/.git/
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
Я хотел записывать деревья через git hash-object, но у меня не получилось,
т. к. там какой-то хитрый формат, причём, кажется — бинарный.
Поэтому тут тоже повторяем команды из мануала:
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ echo 'new file' > new.txt
$ git update-index --add \
--cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
3c4e9cd789d88d8d89c1073707c3585e41b0e614
Теперь коммиты:
Эксперимент № 1
Зафиксируем тело коммита в файле:
$ cat > commit-content.txt
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
First commit
И, запишем коммит через git hash-object:
$ cat commit-content.txt | git hash-object -w --stdin -t commit
К сожалению, хэш коммита получается равен
либо 70d4408b5020e81d19906d6abdd87a73233ebf34,
либо c5c23f68145bf32dedda1929153b3be544b72186, в зависимости от того,
оставляем ли мы перевод строки в конце файла commit-content.txt,
так что воспроизвести хэши из мануала не получилось. Эксперимент № 1 провалился.
Мне нравится думать, что они там напутали с датой или автором. Но тем не менее мы добились стабильности. То есть, мы можем создать коммит с любой желаемой датой и, зафиксировав дату и содержимое, получать стабильный хэш. Поэтому попробуем воспроизвести какой-нибудь другой коммит:
Эксперимент № 2
Идём на сайт, где лежит репозиторий Хромиума. Ищем там коммит попроще: без мёрджей и с коротким описанием. Благо он там на ребэйзах построен и не ветвится. Короткие описания там на релизных коммитах.
В общем, берём коммит, помеченый релзиным тэгом, к тому же тэг нам пригодится на следующем шаге. И возьмём коммит поближе к началу, это тоже пригодится.
Возьмём этот: 7eeeda7f8.
Попробуем сформировать содержание коммита на основе того, что мы видим на гитхабе:

$ cat > commit.txt
tree ???
parent 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
author gitdeps <???> 1396893478 +?
committer gitdeps <???> 1396893478 +?
Publish DEPS for Chromium 4.1.249.0
Вопросиками обозначено то, чего мы не видим на сайте гитхаба: ссылку на дерево, почту авторов и их таймзоны. Придётся клонировать.
Чтобы не клонировать все 18G мы склонируем только один коммит. Если бы мы брали недавний коммит, то пришлось бы скачать 1.12G, чего с мобильного интернета делать не хочется. Поэтому коммит взяли старый и скачали 200М:
$ mkdir ../chromium && cd ../chromium
$ git clone -b 4.1.249.0 --bare --single-branch --depth=1 \
https://github.com/chromium/chromium.git \
.
Cloning into bare repository 'chromium.git'...
remote: Enumerating objects: 37083, done.
remote: Counting objects: 100% (37083/37083), done.
remote: Compressing objects: 100% (21890/21890), done.
remote: Total 37083 (delta 8320), reused 27284 (delta 6297), pack-reused 0
Receiving objects: 100% (37083/37083), 207.95 MiB | 960.00 KiB/s, done.
Resolving deltas: 100% (8320/8320), done.
Возьмём недостающие данные:
$ git cat-file commit 7eeeda7f8 > ../myrepo/commit.txt
$ cd ../myrepo
$ cat commit.txt
tree a1f14424cda2c9638715ca04723fae552d45b8b2
parent 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
author gitdeps <gitdeps@chromium.org> 1396893478 -0700
committer gitdeps <gitdeps@chromium.org> 1396893478 -0700
Publish DEPS for Chromium 4.1.249.0
Построим хэш коммита:
$ cat commit.txt | git hash-object --stdin -t commit
7eeeda7f8ef5338c4dd4f58805e672dd2815192d
Ололо, хэш совпал! Можно считать, что эксперимент № 2 удался!
Заметим, что на предыдущем шаге мы лишь вычисляли хэш объекта, но не создавали объект:
$ git cat-file -p 7eeeda7f8
fatal: Not a valid object name 7eeeda7f8
Давайте создадим:
$ cat commit.txt | git hash-object -w --stdin -t commit
7eeeda7f8ef5338c4dd4f58805e672dd2815192d
$ git cat-file -p 7eeeda7f8
tree a1f14424cda2c9638715ca04723fae552d45b8b2
parent 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
author gitdeps <gitdeps@chromium.org> 1396893478 -0700
committer gitdeps <gitdeps@chromium.org> 1396893478 -0700
Publish DEPS for Chromium 4.1.249.0
Получается, что теперь у нас есть коммит, который указывает на дерево и родителя, которых нет:
$ git log 7eeeda7f8
error: Could not read 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
fatal: Failed to traverse parents of commit 7eeeda7f8ef5338c4dd4f58805e672dd2815192d
$ git diff 7eeeda7f8~ 7eeeda7f8
fatal: bad object 7eeeda7f8~
$ git rev-parse 7eeeda7f8~
5cbf1bd250e30d058f5a3729ecbcf1784497b82d
$ git rev-parse 7eeeda7f8~2
error: Could not read 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
7eeeda7f8~2
error: Could not read 5cbf1bd250e30d058f5a3729ecbcf1784497b82d
fatal: ambiguous argument '7eeeda7f8~2': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
Собственно, конспект
Git — это key-value storage. Это значит, что вы можете запихать туда любое содержимое, а вам вернут ключ, по которому вы сможете до этого содержимого добраться.
Blob
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
-w значит, не просто вернуть хэш, а записать объект в репо.
Теперь мы можем его найти и прочитать:
$ find .git/objects -type f | grep d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
Мы можем просто скопировать файл в другой репозиторий и прочитать там:
$ mkdir ../r2
$ git init ../r2
$ mkdir ../r2/.git/objects/d6
$ cp .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 ../r2/.git/object/d6
$ cd ../r2
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
Такой вид объектов называется blob:
$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob
Дерево
Дерево можно прочитать той же командой, которой читается blob:
$ git cat-file -t master^{tree}
tree
$ git cat-file -p master^{tree}
100644 blob a3b73140d39021a126ee59b6e6b4e2cc3c0f524a dependabot.yml
040000 tree 91502d50eb5ca1ad80db524dbbda0c457888b8c7 workflows
$ git cat-file -p 91502d50eb5ca1ad80db524dbbda0c457888b8c7
100644 blob 7442eb047cb191e8f3690a232fff122eb0fd0c09 create-images.yml
100755 blob 4fad101da29dd2cf5e887680bc829acba50f0a47 docker-tag.sh
Для блобов в дереве доступно всего три режима:
100644обычный файл,100755исполняемый,120000симлинк.
Создадим дерево искуственно в свежем репозитории, который разбирался в пером разделе:
$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
В примере выше мы добавили в дерево существующий объект, но могли добавить и файл с диска:
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
Заметим, что команда write-tree не сбрасывает индекс, там всё остаётся,
повторый вызов write-tree вернёт тот же хэш, а дальнейшие манипуляции
проводятся поверх того, что уже есть в индексе.
Также состояние индекса отражается в выводе команды git status.
Также можно добавить в индекс сущетсвующее дерево поддеревом с префиксом:
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
Коммит
Чтобы создать коммит, вы пишете git commit-tree <the-tree> [-p <the-prev-commit-if-any>].
Коментарий к коммиту он читает с входящего потока и… попросит вас указать
user.name и user.email:
$ echo 'First commit' | git commit-tree d8329f
fae718fda81cd0c08801990326c44458a9ff6505
$ git cat-file -p fae718fda81cd0c08801990326c44458a9ff6505
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Chivorotkiv <git@chiv.info> 1617079735 +0600
committer Chivorotkiv <git@chiv.info> 1617079735 +0600
First commit
Создадим цепочку:
$ echo 'Second commit' | git commit-tree 0155eb -p fae718f
e287117b74e035d69d863b79e16b118c63c94ba4
$ echo 'Third commit' | git commit-tree 3c4e9c -p e28711
b3231612e3d767edc33552968ea054b7efcba42a
Можем посмотреть лог:
$ git log --stat b323161
commit b3231612e3d767edc33552968ea054b7efcba42a
Author: Chivorotkiv <git@chiv.info>
Date: Tue Mar 30 11:05:13 2021 +0600
Third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit e287117b74e035d69d863b79e16b118c63c94ba4
Author: Chivorotkiv <git@chiv.info>
Date: Tue Mar 30 11:04:39 2021 +0600
Second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit fae718fda81cd0c08801990326c44458a9ff6505
Author: Chivorotkiv <git@chiv.info>
Date: Tue Mar 30 10:48:55 2021 +0600
First commit
test.txt | 1 +
1 file changed, 1 insertion(+)
На этом этапе стоит сходить в оригинал, чтобы посмотреть картинки.
Тэги
Этот раздел сделан на основе другой статьи: Curious git — Types of git objects
Давайте создадим новй репо, чтобы проще было и добавим туда файл и коммит:
mkdir example_repo
cd example_repo
git init
echo "An example file" > example_file.txt
git add example_file.txt
git commit -m "An example commit"
После этих действий у нас будет три объекта:
$ find .git/objects -type f
.git/objects/2f/781156939ad540b2434d012446154321e41e03
.git/objects/47/91d80a10ffe91ec1f560d0cd602d6786734316
.git/objects/83/207f0274383b4a79ff6d6c297e95204ba961bc
Просто тэг — это не объект. Добавим просто тэг:
$ git tag just-a-tag
В списке тэгов он есть:
$ git tag
just-a-tag
Но объектов по прежнему три:
$ find .git/objects -type f
.git/objects/2f/781156939ad540b2434d012446154321e41e03
.git/objects/47/91d80a10ffe91ec1f560d0cd602d6786734316
.git/objects/83/207f0274383b4a79ff6d6c297e95204ba961bc
Тэг с аннотацией, это объект. Добавим тэг, тэга будет два, а объекта четыре:
$ git tag -a first-commit -m "Tag pointing to first commit"
$ git tag
first-commit
just-a-tag
$ find .git/objects -type f
.git/objects/2f/781156939ad540b2434d012446154321e41e03
.git/objects/47/91d80a10ffe91ec1f560d0cd602d6786734316
.git/objects/79/602d4b3e0facdf5f474e2239c70af7f3381a1c
.git/objects/83/207f0274383b4a79ff6d6c297e95204ba961bc
Исследуем его:
$ git cat-file -t 79602d4b3e0facdf5f474e2239c70af7f3381a1c
tag
$ git cat-file -p 79602d4b3e0facdf5f474e2239c70af7f3381a1c
object 4791d80a10ffe91ec1f560d0cd602d6786734316
type commit
tag first-commit
tagger ch <ch> 1619026912 +0600
Tag pointing to first commit
$ printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" \
| cat - .git/objects/79/602d4b3e0facdf5f474e2239c70af7f3381a1c \
| gzip -dcq
tag 139object 4791d80a10ffe91ec1f560d0cd602d6786734316
type commit
tag first-commit
tagger ch <ch> 1619026912 +0600
Tag pointing to first commit
Тэг можно добавить не только на коммит, но и на любой другой объект:
На другой тэг:
$ git tag -a tag-a-tag -m "tag some tag" 79602d4b3e0facdf5f474e2239c70af7f3381a1c
hint: You have created a nested tag. The object referred to by your new tag is
hint: already a tag. If you meant to tag the object that it points to, use:
hint:
hint: git tag -f tag-a-tag 79602d4b3e0facdf5f474e2239c70af7f3381a1c^{}
hint: Disable this message with "git config advice.nestedTag false"
$ git cat-file -t 1bb62925ef5653d2d6dce763683c33f131b9303e
tag
$ git cat-file -p 1bb62925ef5653d2d6dce763683c33f131b9303e
object 79602d4b3e0facdf5f474e2239c70af7f3381a1c
type tag
tag tag-a-tag
tagger ch <ch> 1619112967 +0600
tag some tag
На дерево:
$ git tag -a tag-a-tree -m "tag some tree" 83207f0274383b4a79ff6d6c297e95204ba961bc
$ git cat-file -t a6c7281905d46efae7d87e83ab396527a04b80d6
tag
$ git cat-file -p a6c7281905d46efae7d87e83ab396527a04b80d6
object 83207f0274383b4a79ff6d6c297e95204ba961bc
type tree
tag tag-a-tree
tagger ch <ch> 1619113119 +0600
tag some tree
На блоб:
$ git tag -a tag-a-blob -m "tag some blob" 2f781156939ad540b2434d012446154321e41e03
$ git cat-file -t 75f7169ad344c90d6a7357f27fe905e8b2376a9d
tag
$ git cat-file -p 75f7169ad344c90d6a7357f27fe905e8b2376a9d
object 2f781156939ad540b2434d012446154321e41e03
type blob
tag tag-a-blob
tagger ch <ch> 1619113243 +0600
tag some blob
Все наши тэги есть в списке:
$ git tag
first-commit
just-a-tag
tag-a-blob
tag-a-tag
tag-a-tree
Как хранятся объекты:
Сначала заголовок: <type> <content.bytesize>\0 —
тип, пробел, длина контента и нулевой байт. blob 16\0
Гит соединяет заголовок с контентом и берёт от него SHA-1, получается хэш объекта:
store = header + content
hash = sha1(store)
Содержимое вместе с заголовком (<store>) сжимается при помощи ZLib.
Затем сохраняется в файл .git/objects/<hash[0,1]>/<hash[2-39]>
Так его можно прочитать (why-1, why-2, why-3):
$ printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" \
| cat - .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 \
| gzip -dcq
blob 13test content
$ printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" \
| cat - .git/objects/b3/231612e3d767edc33552968ea054b7efcba42a \
| gzip -dcq
commit 215tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614
parent e287117b74e035d69d863b79e16b118c63c94ba4
author Chivorotkiv <git@chiv.info> 1617080713 +0600
committer Chivorotkiv <git@chiv.info> 1617080713 +0600
Third commit
Полезно почитать
git cat-file --helpgit hash-object --help