3.3. Модель git
3.3.1. Наивное управление версиями
В этом разделе рассмотрим процесс создания модели СКВ git с поддержкой создания коммитов, связанных с разными версиями файлов и папок, хранящихся в контентно-адресуемой файловой системе [36], а также вывода истории коммитов в виде графа.
Начнём с рассмотрения наивного подхода к управлению версиями проекта с исходным кодом, основанного на сохранении разных версий файлов в разных папках, без использования СКВ. Создадим в командной оболочке Linux несколько версий простого проекта, содержащего файл с исходным кодом, файл с лицензией и файл с документацией:
~$ mkdir v1
~$ echo "MIT" > v1/license
~$ tree --noreport v1
v1
└── license
~$ cp -r v1 v2
~$ echo "app" > v2/readme.md
~$ tree --noreport v2
v2
├── license
└── readme.md
~$ cp -r v1 v3
~$ mkdir v3/src
~$ echo "print('hello')" > v3/src/hello.py
~$ tree --noreport v3
v3
├── license
└── src
└── hello.py
~$ cp -r v3 v4
~$ cp -r v2/readme.md v4
~$ tree --noreport v4
v4
├── license
├── readme.md
└── src
└── hello.py
Первой версией нашего проекта станет папка v1 с файлом license, содержащим внутри строку MIT, записанную в файл при помощи перенаправления вывода. Вторая версия — папка v2, основанная на v1, но с новым файлом readme.md. Третья версия, в свою очередь, — это папка v3, созданная на основе v1, но с новой папкой src внутри, содержащей файл hello.py с программой на языке Python. Четвёртая версия — папка v4, содержащая результат объединения версий v2 и v3. Команда tree с параметром --noreport печатает в stdout дерево файлов и папок без дополнительных сведений о количестве файлов в папке.
Создание новых версий проекта на основе уже существующих выполняется копированием папки с проектом при помощи команды cp, а слияние версий выполняется вручную. Очевидно, что использование такого подхода трудозатратно и неэффективно — для проекта с множеством версий придётся создать множество папок, а информация, хранящаяся в них, будет повторяться.
3.3.2. Управление версиями в git
Распределённая СКВ git позволяет отслеживать изменения файлов репозитория и в любой момент переключаться между его версиями. С git легко вести работу над одним проектом одновременно и независимо сразу нескольким программистам, с последующим объединением разных версий репозитория в одну при помощи команды git merge. Такая функциональность обеспечивается за счёт сохранения всех версий файлов репозитория на компьютере каждого программиста в специальном формате — в контентно-адресуемой файловой системе (Content-Addressable Storage, CAS) [36].
Использование git позволит обойтись без создания 4 папок:
~$ mkdir repo
~$ cd repo
~/repo$ git init
Initialized empty Git repository in ~/repo/.git/
~/repo$ echo "MIT" > license
~/repo$ git add .
~/repo$ git commit -qm "Init"
~/repo$ tree --noreport
.
└── license
~/repo$ git checkout -b docs
Switched to a new branch 'docs'
~/repo$ echo "app" > readme.md
~/repo$ git add .
~/repo$ git commit -qm "Docs"
~/repo$ tree --noreport
.
├── license
└── readme.md
~/repo$ git checkout master
Switched to branch 'master'
~/repo$ mkdir src
~/repo$ echo "print('hello')" > src/hello.py
~/repo$ git add .
~/repo$ git commit -qm "Code"
~/repo$ tree --noreport
.
├── license
└── src
└── hello.py
~/repo$ git merge docs -qm "Merge"
~/repo$ tree --noreport
.
├── license
├── readme.md
└── src
└── hello.py
При помощи git init мы создали новый репозиторий в папке repo. Затем добавили в него файл license, добавили файл в область индексирования командой git add . и сделали первый коммит Init, связанный с текущим набором файлов в репозитории. Команда git add . добавляет в область индексирования всё содержимое текущей директории. Опция -m команды git commit позволяет задать текст сообщения к коммиту, а опция -q отключает вывод подробных сведений об операции в stdout.
После коммита Init мы создали новую ветку docs при помощи команды git checkout с опцией -b — опция -b указывает, что необходимо создать новую ветку с заданным именем, а не переключиться на существующую. После добавления файла readme.md мы сделали первый коммит в ветке docs и переключились на ветку master, используемую по умолчанию. Затем мы создали новый файл hello.py в новой папке src и сделали коммит Code в ветке master, после чего выполнили слияние веток master и docs при помощи git merge, создав ещё один коммит.
Граф коммитов нашего git-репозитория легко визуализировать, воспользовавшись командой git log с опцией --graph:
~/repo$ git log --graph --oneline
* 12c5bb6 (HEAD -> master) Merge
|\
| * 7ce8f07 (docs) Docs
* | 1071c39 Code
|/
* f0e0c14 Init
В выведенном графе вершины обозначены символом *, справа от каждой вершины указаны первые 7 символов хэш-значений коммитов и сообщения к коммитам, а в круглых скобках справа от последнего коммита в ветке указано имя ветки. Метка HEAD указывается напротив активного в настоящий момент коммита. Опция --oneline команды git log позволяет вывести историю коммитов в краткой форме.
3.3.3. Модель git на Python
Теперь перейдём к созданию модели git на языке Python. Начнём с реализации контентно-адресуемой файловой системы [36] — представим её в виде словаря objects, ключом в котором является хэш-значение, вычисляемое для сохраняемого объекта. Сохраняемый объект, в свою очередь, представляет из себя пару, состоящую из заголовка и данных. Заголовок содержит тип объекта. Формат данных объекта зависит от типа объекта.
Создадим файл git.py и поместим в него следующий код:
from pprint import pprint
objects = {}
def make_object(header, content):
data = (header, content)
h = hash(data)
objects[h] = data
return h
def make_blob(data):
return make_object('blob', data)
make_blob('app')
make_blob("print('hello')")
pprint(objects)
Функция make_object создаёт объект data и сохраняет его в словаре objects. Объект — это пара, состоящая из заголовка header и содержимого content. Затем вычисляется хэш-значение для полученной пары (header, content) при помощи стандартной функции hash, возвращающей целое. Хэш-значение используется в качестве ключа объекта в словаре objects.
Функция make_blob специализирует функцию make_object для сохранения в словаре objects содержимого файла. Функция pprint из стандартного модуля pprint позволяет вывести в stdout структуру данных с автоматически расставленными отступами.
Проверим работу git.py:
~$ python git.py
{-5552424307104227732: ('blob', "print('hello')"),
-2447526263321356918: ('blob', 'app')}
Теперь реализуем рекурсивную функцию make_tree, которая сохраняет всё содержимое заданной папки в словарь objects. Добавим следующий код в файл git.py, заменив при этом вызов make_blob на вызов make_tree в конце файла git.py:
import os
def make_tree(path):
table = []
for name in os.listdir(path):
p = os.path.join(path, name)
if os.path.isfile(p):
with open(p) as f:
table.append((name, make_blob(f.read())))
elif os.path.isdir(p):
table.append((name, make_tree(p)))
return make_object('tree', tuple(table))
make_tree('v4')
pprint(objects)
Функция make_tree читает содержимое папки, доступной по пути path, при помощи функции listdir из стандартного модуля os [12]. Содержимое папки сохраняется в словарь objects как таблица table, причём в первой колонке таблицы указывается имя файла или папки name, а во второй колонке — хэш-значение объекта, связанного с именем из первой колонки таблицы, полученное или от функции make_tree, или от функции make_blob. Вызов функции make_tree с аргументом v4 сохраняет в словарь objects содержимое папки с именем v4.
Проверим работу обновлённой модели git:
~$ tree --noreport v4
v4
├── readme.md
└── src
└── hello.py
~$ python git.py
{1565835151721922343: ('blob', "print('hello')"),
5218520276485572056: ('blob', 'app'),
6679246622821842587: ('blob', 'MIT'),
8983795902379471365: ('tree', (
('readme.md', 5218520276485572056),
('license', 6679246622821842587),
('src', 9119572560804270185))),
9119572560804270185: ('tree', (
('hello.py', 1565835151721922343),))}
Словарь objects содержит граф, показанный на рис. 28.
Добавим поддержку коммитов в модель git. Сведения о коммитах также сохраним в словаре objects, при этом с каждым коммитом будет связано имя автора коммита, сообщение к коммиту, а также папка папка, содержащая одну из версий репозитория. Теперь объекты, хранящиеся в нашей контентно-адресуемой файловой системе, могут быть трёх типов: commit, tree и blob.
Попробуем создать несколько коммитов, с каждым из которых связано содержимое одной из ранее созданных папок v1, v2, v3 и v4.
Обновим файл git.py и добавим в него следующий код:
def make_commit(tree, parents, author, message):
return make_object('commit', (tree, parents, author, message))
c1 = make_commit(make_tree('v1'), (), 'Peter', 'Init')
c2 = make_commit(make_tree('v2'), (c1,), 'Arty', 'Docs')
c3 = make_commit(make_tree('v3'), (c1,), 'Peter', 'Code')
c4 = make_commit(make_tree('v4'), (c2, c3), 'Peter', 'Merge')
pprint(objects)
Функция make_commit специализирует функцию make_object для создания объекта с типом commit в словаре objects. Вызов функции make_commit приводит к созданию нового коммита, связанного с папкой, уже сохранённой в словаре objects в результате вызова функции make_tree.
Словарь objects теперь содержит граф, показанный на рис. 29.
Круглые вершины на рис. 29 соответствуют коммитам, а прямоугольные — объектам типа tree или blob. У каждого коммита может быть один или несколько связанных с ним родительских коммитов. Коммит, с которым связано несколько родительских коммитов — это результат слияния коммитов.
Поскольку ключами в словаре objects являются хэш-значения, вычисленные для содержимого файлов или папок, в objects не может существовать несколько объектов типа blob или tree с одинаковым содержимым. Вследствие этого в вершины графа, соответствующие объектам типа blob, может входить несколько стрелок. Как показано на рис. 29, объекты типа tree могут переиспользоваться схожим образом, однако для каждого коммита создаётся свой объект tree с обновлённым списком файлов и папок, поскольку коммит изменяет содержимое репозитория.
3.3.4. Упражнения
Задача 1. Создайте визуализатор содержимого словаря objects с использованием инструмента Graphviz [14], позволяющий получить изображение графа коммитов, показанного на рис. 29.
Задача 2. Реализуйте на основе разработанной модели git инструмент командной строки, поддерживающий создание коммитов с указанием автора коммита и сообщения к коммиту, а также переключение на нужную версию репозитория по хэш-значению коммита.
Задача 3. Реализуйте в модели git операции для создания веток и переключения между ними.
Задача 4. Реализуйте в модели git операции merge и rebase.
Задача 5. Сделайте из модели git настоящую СКВ: для хранения репозитория и объектов используйте БД sqlite.