понедельник, 29 марта 2010 г.

Git. part 3

Взято здесь

В первой части я уже упоминал, что репозиторий git представляет собой картотеку объектов, объединенных ссылками друг на друга.

Из четырех типов объектов в git (blob, tree, commit, tag) только blob-ы не могут содержать ссылки. Все остальные объекты, по сути, являются просто ссылками либо на blob-ы, либо на другие ссылки.


Мы уже знаем, что blob-ы включают в себя только содержание файла, но не его имя, или режимы доступа. Вся информация об именах содержится в объектах-деревьях (tree). Фактически, деревья аналогичны понятию "каталог" в файловой системе, так же как blob-ы аналогичны понятию inode.

Объекты-деревья могут хранить внутри себя как ссылки на blob-ы, так и ссылки на другие объекты-деревья. В результате можно построить иерархию деревьев, аналогичную иерархии каталогов и файлов.

Объект-дерево представляет собой список элементов, состоящих из четырех полей:
  1. mode (режим доступа) - представляет собой права UNIX на объект-ссылку, плюс несколько дополнительных битов, позволяющих хранить в гите символические ссылки. Записывается в виде шести цифр, из которых первые три описывают тип объекта, а оставшиеся - права UNIX. Правда мне ни разу не удалось увидеть, чтобы значение третьей цифры было отлично от нуля, так что я не знаю что она означает. Первая цифра - 1 для файлов и символических, 0 для директорий. Вторая цифра - 0 для файлов, 2 для символических ссылок, 4 - для директорий
  2. Тип объекта, на который ссылается элемент списка. Может быть blob или tree
  3. SHA1 хеш объекта. Собственно, это и является ссылкой, так как однозначно определяет объект в репозитории git.
  4. Имя объекта. Имя файла для blob-ов, имя директории для tree.
Объект-дерево после создания получает свое имя-хеш, и может быть после этого включен в другие деревья.

Создать новый объект-дерево можно с нуля, используя команду git-mktree. Ей на вход (stdin) надо передать текстовый список, в котором каждая строчка описывает один элемент. Первые три поля должны быть разделены пробелами, а последнее - имя объекта - должно быть отделено табом.

Вот пример:
$ mkdir ~/tmp/gitgut3
$ cd ~/tmp/gitgut3
$ git-init
Initialized empty Git repository in .git/

$ echo "File1" > file1
$ echo "File2" > file2

$ git-hash-object -w file1
03f128cf48cb203d938805e9f3e13b808d1773e9
$ git-hash-object -w file2
b973e639605e63466ea5ba09b04a545f16946ca8

$ echo -e "100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9\tfile1
100640 blob b973e639605e63466ea5ba09b04a545f16946ca8\tfile2" | git-mktree

b2efb2a7e48025c4d185080412a6ba1121ee6c59

Как видно из примера, команде git-mktree нужно подать на стандартный вход содержимое создавамого объекта-дерева, что я и сделал командой echo.
Полученный объект-дерево теперь присутствует в базе:

$ ls .git/objects/b2/efb2a7e48025c4d185080412a6ba1121ee6c59
.git/objects/b2/efb2a7e48025c4d185080412a6ba1121ee6c59

Его содержимое можно посмотреть, используя команду git-ls-tree

$ git-ls-tree b2efb2a7e48025c4d185080412a6ba1121ee6c59
100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9 file1
100640 blob b973e639605e63466ea5ba09b04a545f16946ca8 file2

Чтобы далеко не уходить, покажу, чем же полезно, что деревья являются именно объектами, с именами-хешами.

Например, если у двух объектов-деревьев одинаковое имя-хеш, что это означает? Что внутренности этих деревьев совпадают! А так как внутренности деревьев - это ссылки на объекты, то это означает что два дерева ссылаются на одни и те же объекты. Которые в свою очередь тоже могут быть деревьями или блобами. Таким образом имя-хеш дерева на самом деле идентифицирует не только "файлы в директории", но и все файлы во всех поддиректориях этой директории - одно имя для всех иерархии!

Это свойство позволяет git-у очень быстро производить сравнение деревьев со сколь угодно сложной иерархией, уровнями вложенности и т.д. без чтения собственно содержимого - blob-ов или tree.

Например, я создаю новое дерево, которое отличается от старого дерева b2efb2a7e4... тем, что в содержимое file2 была добавлена дополнительная строчка, а файл file1 переименован в file3.

$ echo Secondline >> file2
$ git-hash-object -w file2
4dd2746869211aedfec0f07afb12a879c09569e7

$ echo -e "100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9\tfile3
100640 blob 4dd2746869211aedfec0f07afb12a879c09569e7\tfile2" | git-mktree

493a5292de0b743e77aa190921da56d33599b59e

$ git-ls-tree 493a5292de0b743e77aa190921da56d33599b59e

100640 blob 4dd2746869211aedfec0f07afb12a879c09569e7 file2
100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9 file3

Давайте посмотрим, как git может легко вычислить разницу между этими деревьями. используя только объекты-деревья.
Для этого сохраним выводы git-ls-tree для каждого дерева в отдельный файл и натравим на них команду diff -u.

$ git-ls-tree b2efb2a7e48025c4d185080412a6ba1121ee6c59 > tree1
$ git-ls-tree 493a5292de0b743e77aa190921da56d33599b59e > tree2
$ diff -u tree1 tree2
--- tree1 2007-08-14 14:55:06 +0400
+++ tree2 2007-08-14 14:55:30 +0400
@@ -1,2 +1,2 @@
-100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9 file1
-100640 blob b973e639605e63466ea5ba09b04a545f16946ca8 file2

+100640 blob 4dd2746869211aedfec0f07afb12a879c09569e7 file2
+100640 blob 03f128cf48cb203d938805e9f3e13b808d1773e9 file3


Итак, видно, что по сравнению с деревом 1 в дереве два исчез file1, у file2 изменился SHA1 хеш, и добавился новый file3.
Также можно заметить, что у удаленного файла file1 и добавленного файла file3 одинаковый SHA1 хеш - отсюда можно сделать вывод, что было произведено переименование из file1 в file3 без изменения содержимого.

Точно такую же работу производит и git, точнее его команда git-diff-tree. Она выводит разницу между двумя деревьями в читабельном для человека виде.

$ git-diff-tree b2efb2a7e48025c4d185080412a6ba1121ee6c59 493a5292de0b743e77aa190921da56d33599b59e
:100644 000000 03f128cf48cb203d938805e9f3e13b808d1773e9 0000000000000000000000000000000000000000 D file1
:100644 100644 b973e639605e63466ea5ba09b04a545f16946ca8 4dd2746869211aedfec0f07afb12a879c09569e7 M file2
:000000 100644 0000000000000000000000000000000000000000 03f128cf48cb203d938805e9f3e13b808d1773e9 A file3

Если git-diff-tree вызывать с ключом -p, то она сгенерирует патч, который будучи применен к tree1, приведет его к tree2.

git-diff-tree -p b2efb2a7e48025c4d185080412a6ba1121ee6c59 493a5292de0b743e77aa190921da56d33599b59e
diff --git a/file1 b/file1
deleted file mode 100644
index 03f128c..0000000
--- a/file1
+++ /dev/null
@@ -1 +0,0 @@
-File1
diff --git a/file2 b/file2
index b973e63..4dd2746 100644
--- a/file2
+++ b/file2
@@ -1 +1,2 @@
File2
+Secondline
diff --git a/file3 b/file3
new file mode 100644
index 0000000..03f128c
--- /dev/null
+++ b/file3
@@ -0,0 +1 @@
+File1

Как видно по патчу, git-diff-tree не учел, что файл file1 был переименован в file3, и сгенерировал патч так, как будто file1 удалили, и file3 добавили заново.

Но как мы знаем, blob-ы у file1 и file3 совпадают - поэтому можно точно сказать что было переименование. Для того, чтобы git-diff-tree стал обращать на это внимание, ему надо передать ключик -M (detect renames).

Тогда он сгенерирует особый патч-переименование. К сожалению, стандартная команда patch не может прикладывать такие патчи-переименования, так что потребуется прикладывать этот патч к дереву с помощью команды git-apply.

$ git-diff-tree -M -p b2efb2a7e48025c4d185080412a6ba1121ee6c59 493a5292de0b743e77aa190921da56d33599b59e
diff --git a/file2 b/file2
index b973e63..4dd2746 100644
--- a/file2
+++ b/file2
@@ -1 +1,2 @@
File2
+Secondline
diff --git a/file1 b/file3
similarity index 100%
rename from file1
rename to file3

Итак, объекты-деревья служат для объединения blob-ов и других деревьев в иерархию, аналогичную файловой системе. Деревья хранят биты доступа, хеши содержимого и имена объектов, поэтому между двумя деревьями может быть разница только по этим параметрам.

Такие параметры как времена создания, изменения и доступа файла, а также создатель или владелец файла, в деревьях не записываются. Некоторое подобие такой информации хранят объекты-commit'ы, о которых я расскажу в следующий раз.

Комментариев нет:

Отправить комментарий