Архитектура git-репозитория

Git-репозиторий представляет собой (грубо говоря) хранилище объектов и ссылок (представленное в виде каталога в файловой системе). Объекты бывают следующих видов:

Из ref-ов интересны следующие:

Продемонстрируем внутреннее устройство git-репозитория на примере.

Сначала создадим новый репозиторий:

% git init
Initialized empty Git repository in .git/

Создадим два файла с каким-нибудь содержимым:

% echo -e "First line\nSecond line\nThird line\nFourth line\n" > file1.txt 
% echo -e "Second file\n-----------\n\nTest.\n" > file2.txt

Теперь добавим объекты blob в репозиторий. Объекты blob суть данные. Идентифицируются объекты SHA1-хэшем от их содержимого (первые два символа в hex-записи хэша используются как имя директории, куда кладётся объект, остальные — как имя файла объекта в этом каталоге). Таким образом, если два объекта совпадают по содержимому, то и идентификаторы у них будут одинаковы (как следствие, сохранён будет только один объект). Благодаря чудесным свойстам хэшей, обратное тоже верно с точностью до коллизий. Для создания blob и добавления его в репозиторий существует команда git hash-object.

% git hash-object -w file1.txt
bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82
% git hash-object -w file2.txt
3fc9237ae2f776560a581562356e6b28ba31db80

Для просмотра объектов существует команда git cat-file.

% git cat-file blob 3fc9237ae2f776560a581562356e6b28ba31db80
Second file
-----------

Test.

Деревья представляют собой объекты с содержимым специального вида — набором записей вида <права> <тип> <идентификатор> <имя> (по записи на строку; в дерево могут входить blob'ы и другие деревья). Идентификатором дерева является SHA1-хэш его содержимого. Для создания дерева и добавления его в репозиторий существует команда git mktree.

% echo -e "100640 blob bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82\tfile1.txt\n100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80\tfile2.txt" | git mktree
eee44d801c82169dd7b7709773607ea989ea4be7

Просмотр дерева осуществляется командой git ls-tree.

% git ls-tree eee44d801c82169dd7b7709773607ea989ea4be7
100640 blob bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82    file1.txt
100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80    file2.txt

Поменяем немного файлы и создадим дерево, их включающее.

% echo -e "Fifth line\n" >> file1.txt

Тут добавляется ещё один объект типа blob в репозиторий (при этом объект с идентификатором bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82 никуда не девается):

% git hash-object -w file1.txt
7e3180bd025517cac5ef63061d39a5f2d6c4df3a

Добавляем ещё одно дерево, с новым blob и указанием другого имени для второго файла:

% echo -e "100640 blob 7e3180bd025517cac5ef63061d39a5f2d6c4df3a\tfile1.txt\n100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80\tfile3.txt" | git-mktree 
43e9938b3671088a7b07c6c8cd8c5e7e5c586196

Сравним деревья.

% git ls-tree eee44d801c82169dd7b7709773607ea989ea4be7 > old_tree
% git ls-tree 43e9938b3671088a7b07c6c8cd8c5e7e5c586196 > new_tree

% diff -u old_tree new_tree
--- old_tree    2011-04-06 17:08:13.000000000 +0400
+++ new_tree    2011-04-06 17:08:21.000000000 +0400
@@ -1,2 +1,2 @@
-100640 blob bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82   file1.txt
-100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80   file2.txt
+100640 blob 7e3180bd025517cac5ef63061d39a5f2d6c4df3a   file1.txt
+100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80   file3.txt

Для сравнения деревьев в git существует специальная команда git diff-tree.

% git diff-tree eee44d801c82169dd7b7709773607ea989ea4be7 43e9938b3671088a7b07c6c8cd8c5e7e5c586196
:100644 100644 bdd1c7494f12d6e63756a1e7b52dc5b1cd67da82 7e3180bd025517cac5ef63061d39a5f2d6c4df3a M      file1.txt
:100644 000000 3fc9237ae2f776560a581562356e6b28ba31db80 0000000000000000000000000000000000000000 D      file2.txt
:000000 100644 0000000000000000000000000000000000000000 3fc9237ae2f776560a581562356e6b28ba31db80 A      file3.txt

% git diff-tree -p eee44d801c82169dd7b7709773607ea989ea4be7 43e9938b3671088a7b07c6c8cd8c5e7e5c586196
diff --git a/file1.txt b/file1.txt
index bdd1c74..7e3180b 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3,3 +3,5 @@ Second line
 Third line
 Fourth line
 
+Fifth line
+
diff --git a/file2.txt b/file2.txt
deleted file mode 100644
index 3fc9237..0000000
--- a/file2.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Second file
------------
-
-Test.
-
diff --git a/file3.txt b/file3.txt
new file mode 100644
index 0000000..3fc9237
--- /dev/null
+++ b/file3.txt
@@ -0,0 +1,5 @@
+Second file
+-----------
+
+Test.
+

Ключ -M позволяет детектировать переименования.

% git diff-tree -p -M eee44d801c82169dd7b7709773607ea989ea4be7 43e9938b3671088a7b07c6c8cd8c5e7e5c586196
diff --git a/file1.txt b/file1.txt
index bdd1c74..7e3180b 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3,3 +3,5 @@ Second line
 Third line
 Fourth line
 
+Fifth line
+
diff --git a/file2.txt b/file3.txt
similarity index 100%
rename from file2.txt
rename to file3.txt

Перейдём к созданию коммитов. Коммит — объект с содержимым в определённом формате:

<Идентификатор объекта-дерева>
<Список родительских коммитов, может быть ноль и более>
<Информация об авторе>
<Информация о коммитере>

<Описание коммита>

Информация о авторе (человеке, явлюяющемся автором изменений) и коммитере (человеке, добавившим коммит с изменениями в репозиторий) берётся из переменных окружения или конфигурационного файла.

% export GIT_AUTHOR_NAME="Author Anonymous"                 
% export GIT_AUTHOR_EMAIL="author@example.org"
% export GIT_COMMITTER_NAME="Committer Anonymous"  
% export GIT_COMMITTER_EMAIL="committer@example.org"
% echo "Initial commit." | git commit-tree eee44d801c82169dd7b7709773607ea989ea4be7
21b04a2213a7c1381c30f5f9705a0e8d2f72b375

% git cat-file commit 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
tree eee44d801c82169dd7b7709773607ea989ea4be7
author Author Anonymous <author@example.org> 1302095470 +0400
committer Committer Anonymous <committer@example.org> 1302095470 +0400

Initial commit.

Идентификатором коммита всё также является хэш его содержимого.

Для просмотра истории коммитов служит команда git log.

% PAGER=cat git log 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
commit 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
Author: Author Anonymous <author@example.org>
Date:   Wed Apr 6 17:11:10 2011 +0400

    Initial commit.

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

% echo "Added fifth line, file2.txt renamed." | git commit-tree 43e9938b3671088a7b07c6c8cd8c5e7e5c586196 -p 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
2bc476f71932bf5cb83ada16cc80c704960da513

% git cat-file commit 2bc476f71932bf5cb83ada16cc80c704960da513
tree 43e9938b3671088a7b07c6c8cd8c5e7e5c586196
parent 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
author Author Anonymous <author@example.org> 1302095586 +0400
committer Committer Anonymous <committer@example.org> 1302095586 +0400

Added fifth line, file2.txt renamed.

% git log 2bc476f71932bf5cb83ada16cc80c704960da513
commit 2bc476f71932bf5cb83ada16cc80c704960da513
Author: Author Anonymous <author@example.org>
Date:   Wed Apr 6 17:13:06 2011 +0400

    Added fifth line, file2.txt renamed.

commit 21b04a2213a7c1381c30f5f9705a0e8d2f72b375
Author: Author Anonymous <author@example.org>
Date:   Wed Apr 6 17:11:10 2011 +0400

    Initial commit.

Заметим, что указав git log второй коммит, он по идентификатору parent в коммите восстановил историю коммитов.

Создадим ссылку-ветку и указатель на текущий коммит (относительно которого развёрнута рабочая директория).

% echo 2bc476f71932bf5cb83ada16cc80c704960da513 > .git/refs/heads/master
% ln -sf refs/heads/master .git/HEAD
% git branch
* master
% git rev-parse master
2bc476f71932bf5cb83ada16cc80c704960da513
% git rev-parse HEAD  
2bc476f71932bf5cb83ada16cc80c704960da513

Команда git rev-parse позволяет получить идентификатор объекта, на который ссылается ref. Как можно видеть, поскольку HEAD есть симлинк на текущий бранч, то они ссылаются на один и тот же коммит.

Создадим второй бранч, переключимся на него и добавим в него новый коммит.

% echo 2bc476f71932bf5cb83ada16cc80c704960da513 > .git/refs/heads/other 
% ln -sf refs/heads/other .git/HEAD                                                
% git branch
  master
* other
% echo -e "Sixth line\n" >> file1.txt
% git hash-object -w file1.txt
1436c3454bd09d96e134546f8dcd3d87feb92970
% echo -e "100640 blob 1436c3454bd09d96e134546f8dcd3d87feb92970\tfile1.txt\n100640 blob 3fc9237ae2f776560a581562356e6b28ba31db80\tfile3.txt" | git mktree
ba1bdfb3c0e54ae124846054b7d6195a3524e89f
% echo "Sixth line added" | git commit-tree ba1bdfb3c0e54ae124846054b7d6195a3524e89f -p 2bc476f71932bf5cb83ada16cc80c704960da513
e076228e058b8edbfc120b4f4515baad783b64b5
% echo e076228e058b8edbfc120b4f4515baad783b64b5 > .git/HEAD

% PAGER=cat git log --pretty=oneline
e076228e058b8edbfc120b4f4515baad783b64b5 Sixth line added
2bc476f71932bf5cb83ada16cc80c704960da513 Added fifth line, file2.txt renamed.
21b04a2213a7c1381c30f5f9705a0e8d2f72b375 Initial commit.

Второй основной вид ссылок — теги. Теги создаются командой git tag.

% git tag fifth-line 2bc476f71932bf5cb83ada16cc80c704960da513 

Это далеко не весь инструментарий Git, только низкоуровневая его часть. Git, как и любая другая приличная (D)VCS также включает множество инструментов для взаимодействия репозиториев, патчинга-мерджа, ретроспекции и преобразований репозитория.

Ссылки

LecturesCMC/ProgAdm2011/02_Git (последним исправлял пользователь eSyr 2011-05-31 21:49:11)