Чтобы на самом деле разобраться в том как Git работает с ветками, мы должны сделать шаг назад и рассмотреть, как Git хранит свои данные. Как вы наверное помните из Главы Введение, Git не хранит данные как последовательность изменений или дельт, а как последовательность снимков состояния (snapshot).
Когда вы фиксируете изменения в Git, Git сохраняет фиксируемый объект, который содержит указатель на снимок содержимого индекса, метаданные автора и комментария и ноль или больше указателей на коммиты, которые были прямыми предками этого коммита: ноль предков для первого коммита, один — для обычного коммита и несколько — для коммита, полученного в результате слияния двух или более веток.
Для наглядности, давайте предположим, что у вас есть каталог, содержащий три файла, и вы их все индексируете и делаете коммит. При подготовке файлов для каждого из них вычисляется контрольная сумма (SHA-1 хеш мы упоминали в Главе Введение), затем эта версия файла сохраняется в Git-репозиторий (Git обращается к ним как к двоичным данным), и эта контрольная сумма добавляется в индекс:
$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'
Когда вы создаёте коммит выполняя git commit, Git вычисляет контрольную сумму каждого подкаталога (в нашем случае, только корневого каталога) и сохраняет объекты для этого дерева в Git-репозиторий. Затем Git создаёт объект для коммита, который имеет метаданные и указатель на корень проектного дерева. Таким образом, Git может воссоздать текущее состояние, когда нужно.
Ваш Git-репозиторий теперь содержит пять объектов: по одному массиву двоичных данных для содержимого каждого из трёх файлов, одно дерево, которое перечисляет содержимое каталога и определяет соответствие имён файлов и массивов двоичных данных, и один коммит с указателем на корень этого дерева и все метаданные коммита. Схематично, данные в вашем Git-репозитории выглядят, как показано на Рисунке 3-1.
Рисунок 3-1. Данные репозитория с единственным коммитом
Рисунок 3-2. Данные объектов Git в случае нескольких коммитов
master
. Когда вы вначале создаёте коммиты, вам даётся ветвь master
, указывающая на последний сделанный коммит. При каждом новом коммите, указатель двигается вперёд автоматически.
Рисунок 3-3. Ветка указывает на историю коммитов
testing
. Это делается командой git branch
:
$ git branch testing
Эта команда создает новый указатель на тот самый коммит, на котором вы сейчас находитесь (см. Рисунок 3-4).
Рисунок 3-4. Несколько веток, указывающих на историю коммитов
HEAD
(верхушка). Учтите, что это сильно отличается от концепции HEAD
в других СУВ, таких как Subversion или CVS, к которым вы возможно привыкли. В Git это указатель на локальную ветку, на которой вы находитесь. В данном случае вы всё ещё на ветке master
. Команда git branch
только создала новую ветвь, она не переключила вас на неё (см. Рисунок 3-5).
Рисунок 3-5. Файл HEAD
указывает на текущую ветку
git checkout
. Давайте перейдем на новую ветку testing
:
$ git checkout testing
Это действие перемещает HEAD
так, чтобы тот указывал на ветку testing
(см. Рисунок 3-6).
Рисунок 3-6. HEAD
указывает на другую ветку, когда вы их переключаете
$ vim test.rb
$ git commit -a -m 'made a change'
На Рисунке 3-7 показан результат.
Рисунок 3-7. Ветка, на которую указывает HEAD
, движется вперёд с каждым коммитом
testing
передвинулась вперёд, но ваша ветвь master
всё ещё указывает на коммит, на котором вы были, когда выполняли git checkout
, чтобы переключить ветки. Давайте перейдём обратно на ветвь master
:
$ git checkout master
На Рисунке 3-8 можно увидеть результат.
Рисунок 3-8. HEAD
перемещается на другую ветку при checkout
’е
HEAD
назад на ветку master
и вернула файлы в вашем рабочем каталоге назад, в соответствие со снимком состояния, на который указывает master
. Это также означает, что изменения, которые вы делаете, начиная с этого момента, будут ответвляться от старой версии проекта. Это полностью откатывает изменения, которые вы временно делали на ветке testing
. Таким образом, дальше вы можете двигаться в другом направлении.
Давайте снова сделаем немного изменений и зафиксируем их:
$ vim test.rb
$ git commit -a -m 'made other changes'
Теперь история вашего проекта разветвилась (см. Рисунок 3-9). Вы создали новую ветку, перешли на неё, поработали на ней немного, переключились обратно на основную ветку и выполнили другую работу. Оба эти изменения изолированы на отдельных ветках: вы можете переключаться туда и обратно между ветвями и слить их, когда будете готовы. И вы сделали всё это простыми командами branch
и checkout
.
Рисунок 3-9. История с разошедшимися ветками