Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.1. Система контроля версий Git

Предположим, программист, незнакомый с инструментами контроля версий, хочет внести новое, экспериментальное изменение в свою программу. Наш программист сохраняет текущее состояние программного проекта в отдельном каталоге и далее занимается нововведениями в своем коде. В какой-то момент оказывается, что экспериментальная идея оказалась неудачной и программист восстанавливает состояние проекта из ранее сохраненного каталога. Такой способ управления версиями проекта требует большой дисциплины и ручного труда. Особенно трудоемкой работа с версиями проекта становится в условиях коллективной разработки. Именно эту работу и призвана автоматизировать система контроля версий.

Система контроля версий (СКВ, Version Control System, VCS) – основной инструмент конфигурационного управления, позволяющий управлять изменениями (версиями) в файлах или иных наборах данных.

Коммит (commit) – фиксация факта изменений в СКВ.

Репозиторий (repository, repo) – место хранения данных проекта, управляемого СКВ.

Ветка (branch) – отдельная копия части репозитория, в которую можно вносить изменения, не влияющие на другие ветки.

Различают следующие типы систем контроля версий (СКВ):

  • локальные системы;
  • централизованные системы;
  • распределенные системы.

Локальные СКВ (local VCS) относятся к самым первым системам контроля версий, которые появились еще в начале 70-х. Локальность означает, что история изменений хранится на компьютере пользователя. В локальных СКВ файлы хранятся в виде патчей – изменений между соседними версиями файла (см. утилиту diff в UNIX), что позволяет экономить дисковое пространство. В коллективном режиме пользователи обмениваются между собой (обычно по электронной почте) патчами. Очевидно, что такой подход к коллективной разработке нельзя назвать удобным.

Централизованные СКВ (centralized VCS) используют клиент-серверную архитектуру. СКВ и связанный с ней репозиторий проекта теперь находятся на сервере. Каждый пользователь на своем локальном компьютере имеет только ту часть общего репозитория, с который непосредственно работает. Такой подход упрощает коллективную разработку, однако проблемы с доступом к серверу СКВ могут затруднить работу всего коллектива.

Распределенные СКВ (distributed VCS) отличаются использованием полной копии проекта на каждом из компьютеров пользователя, что обеспечивает лучшую сохранность проекта, чем в случае централизованных СКВ. В распределенных СКВ могут использоваться различные схемы взаимодействия между удаленными репозиториями и, в частности, может моделироваться работа по клиент-серверной модели.

При совместной работе над одними и теми же файлами неизбежно возникают конфликты доступа к данным. В СКВ используются следующие способы разрешения конфликтов:

  • блокировка доступа к файлу первым пользователем, который к нему обратился;
  • использование локальных копий файла у каждого пользователя с последующим слиянием общих результатов работы в автоматическом или ручном режиме.

В табл. 3 представлены три поколения СКВ.

Таблица 3. Поколения систем контроля версий
Поколение Модель взаимодействия Единица операции Примеры
1 Локальная Файл SCCS, RCS
2 Централизованная Файл / множество файлов CVS, SourceSafe, Subversion, Team Foundation Server
3 Распределенная Множество файлов Bazaar, Git, Mercurial, Fossil

История развития СКВ показана на рис. 21.

Рисунок 21. История развития систем контроля версий

Git [36] является децентрализованной СКВ. Разработана эта система была Л. Торвальдсом в 2005 году для нужд управления версиями ядра ОС Linux. Сегодня Git является самой популярной СКВ. Работу с Git не назовешь простой и многие пользователи критикуют эту систему за неудобный интерфейс командной строки. Тем не менее, основные архитектурные решения в Git являются изящными и логичными, но для того, чтобы их оценить, необходимо узнать, как работает Git изнутри.

3.1.1. Простейшие команды Git

Рассмотрим сначала самые распространенные команды Git.

Создание Git-репозитория в текущем каталоге:

~$ mkdir my_repo
~$ cd my_repo
~/my_repo$ git init .
Initialized empty Git repository in /root/my_repo/.git/

Состояние git-репозитория:

~/my_repo$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

В данном случае Git сообщает очевидное – в проекте еще не было коммитов.

Создадим теперь первый файл в репозитории:

~/my_repo$ echo "# Some text" > readme.md
~/my_repo$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        readme.md

nothing added to commit but untracked files present (use "git add" to track)

Теперь информация о статусе репозитория изменилась – появился неотслеживаемый файл readme.md.

В Git файлы могут находиться в следующих состояниях:

  • неотслеживаемые файлы (untracked) – файлы, которые Git не учитывает в своей работе,
  • измененные файлы (modified) – отслеживаемые файлы, содержимое которых было изменено, но не добавлено в область индексирования;
  • индексированные файлы (staged) – измененные файлы, добавленные в область индексирования;
  • зафиксированные файлы (commited) – файлы, добавленные в коммит.

Таким образом, при создании нового коммита сначала нужно добавить выбранные файлы в специальную промежуточную зону – область индексирования.

Проиндексировать файлы можно с помощью команды Git add:

~/my_repo$ git add readme.md
~/my_repo$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   readme.md

После добавления всех необходимых файлов в зону индекса (это можно сделать одной командой: git add .) создается коммит, но если в Git еще не заданы данные пользователя, то необходимо сначала их указать:

~/my_repo$ git config --local user.name "Peter"
~/my_repo$ git config --local user.email "peter@example.com"
~/my_repo$ git commit -m "first commit"
[master (root-commit) 3ba9fa7] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 readme.md

Теперь первая версия проекта зафиксирована. Информацию о коммитах выдает следующая команда:

~/my_repo$ git log
commit 3ba9fa7980a4ba36086e66389b1ef95cbbf317e2 (HEAD -> master)
Author: Peter <peter@example.com>
Date:   Tue Nov 16 17:08:47 2021 +0300

    first commit

Обратите внимание на длинную последовательность 3ba9fa.... Это хеш-значение коммита, определяющее текущую версию репозитория. Текущая ветка проекта является главной и традиционно называется master или main.

Работу с версиями в Git можно изобразить в виде графа коммитов, см. рис. 22.

Рисунок 22. Состояние репозитория после нескольких коммитов

3.1.2. Ветвление в Git

Традиционно ветвление в СКВ позволяет разделять проект на независимые сущности (ветки), где изменения в конкретной ветке не затрагивают остальные ветки. Это полезно в условиях коллективной разработки, когда программисты одновременно работают над разными частями программы. Представим ситуацию, когда есть задача по разработке новой функциональности, но необходимо параллельно вносить и исправления в проект, не затрагивая новые функции. В таких ситуациях используется ветвление: под новые задачи создается отдельная ветка, и разработка ведется в ней.

Рассмотрим на примерах ветвление в Git.

Новая ветка создается с помощью команды git branch имя. Переключиться на ветку можно с помощью git checkout имя. Так как создание ветки и переключение на нее – зачастую следующие друг за другом операции, их можно выполнить одной командой git checkout -b имя.

Предположим, работа над репозиторием my_repo развивалась следующим образом:

git branch tests
git add ...
git commit -m "..."
git checkout tests
git add ...
git commit -m "..."
git add ...
git commit -m "..."

На рис. 23 показано новое состояние графа коммитов репозитория.

Рисунок 23. Работа с дополнительной веткой

В какой-то момент ветки сливаются (merge):

git checkout master
git merge tests

На рис. 24 показан результат этого слияния. Был создан новый коммит, объединяющий в себе изменения из обеих веток.

Так как ветки master и tests указывают на один и тот же коммит, то ветку test, если она больше не нужна, можно удалить командой git branch -d tests.

Рисунок 24. Слияние веток

Еще одним способом объединения веток является перебазирование, осуществляемое командой git rebase:

git checkout master
git rebase tests

На рис. 25 показан результат перебазирования ветки tests на master. Здесь изменения, созданные в tests, были применены поверх master. В результате получена линейная история коммитов, которую, зачастую, изучать проще, чем результат, полученный с помощью merge.

Рисунок 25. Перебазирование веток

3.1.3. Git изнутри

Все служебную информацию о репозитории Git хранит в подкаталоге .git. Основой Git является таблица объектов, адресуемая по ключам – хеш-значениям этих объектов. Такая схема хранения позволяет автоматически задавать уникальную версию для каждого файла (эта версия определяется ключом в таблице, то есть хеш-значением содержимого файла), а также дает возможность избежать дублирования файлов с одинаковым содержимым.

В Git используются следующие типы объектов:

  • blob (binary large object) – содержимое файлов репозитория,
  • дерево – текущее состояние или снимок файловой иерархии репозитория,
  • коммит – информация о коммите.

На рис. 26 показано более детальное состояние репозитория для примера с перебазированием из предыдущего раздела.

Рисунок 26. Взаимосвязи объектов в репозитории

Попробуем найти в нашем тестовом репозитории my_repo (в его состоянии на момент первого коммита) информацию о хеш-значениях веток:

$ cd .git
~/my_repo/.git$ ls
COMMIT_EDITMSG  HEAD  branches  config  description  hooks  index  info  logs  objects  refs
~/my_repo/.git$ cd refs
~/my_repo/.git/refs$ ls
heads  tags
~/my_repo/.git/refs$ cd heads
~/my_repo/.git/refs/heads$ ls
master
~/my_repo/.git/refs/heads$ cat master
3ba9fa7980a4ba36086e66389b1ef95cbbf317e2

Зная хеш-значение объекта master можно попробовать найти его в таблице объектов:

~/my_repo/.git/refs/heads$ cd ..
~/my_repo/.git/refs$ cd ..
~/my_repo/.git$ cd objects/
~/.git/objects$ ls
07  3b  7d  info  pack
~/my_repo/.git/objects$ cd 3b
~/my_repo/.git/objects/3b$ ls
a9fa7980a4ba36086e66389b1ef95cbbf317e2

Подкаталоги с числами в objects указывают на начальную часть хеш-значения объекта, сам же файл объекта можно найти соответствующего подкаталога.

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

~/my_repo$ git cat-file -p 3ba9fa7980a4ba36086e66389b1ef95cbbf317e2
tree 074f8b59918b080288259854fcf875a6b8e543fe
author Peter <peter@example.com> 1637071727 +0300
committer Peter <peter@example.com> 1637071727 +0300

first commit

Был выдан объект коммита. Объекты этого типа включают в себя:

  • хеш-значение связанного с коммитом объекта дерева;
  • хеш-значения родителей коммита (в рассматриваемом случае коммит единственный, поэтому информация о его родителях не показана);
  • метаданные, в том числе указание авторства и текст коммита.

Попробуем теперь изучить объект дерева по его полученному хеш-значению:

~/my_repo$ git cat-file -p 074f8b59918b080288259854fcf875a6b8e543fe
100644 blob 7dfce3922d94e459d1545a9fc568be0369eaa973    readme.md

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

  • хеш-значения blob-объектов;
  • хеш-значения объектов деревьев;
  • указание прав доступа к файлам и каталогам.

Blob-объект для readme.md можно изучить аналогичным образом.

Обратите внимание, что в Git хеш-значение единственного коммита характеризует не только репозиторий на момент совершения коммита, но и всю предшествующую над ним работу. Это достигается благодаря использованию в Git иерархии хеш-значений («хеш-значения от хеш-значений»). Благодаря такой организации данных любые внесенные искажения в репозиторий или в одну из его предыдущих версий могут быть немедленно выявлены.