Как работает Git

Original author: Mary Rose Cook
  • Translation
В этом эссе описана схема работы Git. Предполагается, что вы знакомы с Git достаточно, чтобы использовать его для контроля версий своих проектов.

Эссе концентрируется на структуре графа, на которой основан Git, и на том, как свойства этого графа определяют поведение Git. Изучая основы, вы строите своё представление на достоверной информации, а не на гипотезах, полученных из экспериментов с API. Правильная модель позволит вам лучше понять, что сделал Git, что он делает и что он собирается сделать.

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

После прочтения для ещё более глубокого погружения можно обратиться к обильно комментируемому исходному коду моей реализации Git на JavaScript.

Создание проекта


~ $ mkdir alpha
~ $ cd alpha

Пользователь создаёт директорию alpha

~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt

Он перемещается в эту директорию и создаёт директорию data. Внутри он создаёт файл letter.txt с содержимым «а». Директория alpha выглядит так:

alpha
└── data
    └── letter.txt

Инициализируем репозиторий


~/alpha $ git init
          Initialized empty Git repository

git init вносит текущую директорию в Git-репозиторий. Для этого он создаёт директорию .git и создаёт в ней несколько файлов. Они определяют всю конфигурацию Git и историю проекта. Это обычные файлы – никакой магии. Пользователь может их читать и редактировать. То есть – пользователь может читать и редактировать историю проекта так же просто, как файлы самого проекта.

Теперь директория alpha выглядит так:

alpha
├── data
|   └── letter.txt
└── .git
    ├── objects
    etc...

Директория .git с её содержимым относится с Git. Все остальные файлы называются рабочей копией и принадлежат пользователю.

Добавляем файлы


~/alpha $ git add data/letter.txt

Пользователь запускает git add на data/letter.txt. Происходят две вещи.

Во-первых, создаётся новый блоб-файл в директории .git/objects/. Он содержит сжатое содержимое data/letter.txt. Его имя – сгенерированный хэш на основе содержимого. К примеру, Git делает хэш от а и получает 2e65efe2a145dda7ee51d1741299f848e5bf752e. Первые два символа хэша используются для имени директории в базе объектов: .git/objects/2e/. Остаток хэша – это имя блоб-файла, содержащего внутренности добавленного файла: .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

Заметьте, что простое добавление файла в Git приводит к сохранению его содержимого в директории objects. Оно будет храниться там, если пользователь удалит data/letter.txt из рабочей копии.

Во-вторых, git add добавляет файл в индекс. Индекс – это список, содержащий все файлы, за которыми Git было наказано следить. Он хранится в .git/index. Каждая строка даёт в соответствие отслеживаемому файлу хэш его содержимого и время добавления. Вот таким получается содержимое индекса после команды git add:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e

Пользователь создаёт файл data/number.txt содержащий 1234.

~/alpha $ printf '1234' > data/number.txt

Рабочая копия выглядит так:

alpha
└── data
    └── letter.txt
    └── number.txt

Пользователь добавляет файл в Git.

~/alpha $ git add data

Команда git add создаёт блоб-файл, в котором хранится содержимое data/number.txt. Он добавляет в индекс запись для data/number.txt, указывающую на блоб. Вот содержимое индекса после повторного запуска git add:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3

Заметьте, что в индексе перечислены только файлы из директории data, хотя пользователь давал команду git add data. Сама директория data отдельно не указывается.

~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data

Когда пользователь создал data/number.txt, он хотел написать 1, а не 1234. Он вносит изменение и снова добавляет файл к индексу. Эта команда создаёт новый блоб с новым содержимым. Она обновляет запись в индексе для data/number.txt с указанием на новый блоб.

Делаем коммит


~/alpha $ git commit -m 'a1'
          [master (root-commit) 774b54a] a1

Пользователь делает коммит a1. Git выводит данные о нём. Мы вскоре объясним их.

У команды commit есть три шага. Она создаёт граф, представляющий содержимое версии проекта, которую коммитят. Она создаёт объект коммита. Она направляет текущую ветку на новый объект коммита.

Создание графа


Git запоминает текущее состояние проекта, создавая древовидный граф из индекса. Этот граф записывает расположение и содержимое каждого файла в проекте.

Граф состоит из двух типов объектов: блобы и деревья. Блобы сохраняются через git add. Они представляют содержимое файлов. Деревья сохраняются при коммите. Дерево представляет директорию в рабочей копии. Ниже приведён объект дерева, записавший содержимое директории data при новом коммите.

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt

Первая строка записывает всё необходимое для воспроизводства data/letter.txt. Первая часть хранит права доступа к файлу. Вторая – что содержимое файла хранится в блобе, а не в дереве. Третья – хэш блоба. Четвёртая – имя файла.

Вторая строчка тем же образом относится к data/number.txt.

Ниже указан объект-дерево для alpha, корневой директории проекта:

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data

Единственная строка указывает на дерево data.



В приведённом графе дерево root указывает на дерево data. Дерево data указывает на блобы для data/letter.txt и data/number.txt.

Создание объекта коммита


git commit создаёт объект коммита после создания графа. Объект коммита – это ещё один текстовый файл в .git/objects/:

tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500

a1

Первая строка указывает на дерево графа. Хэш – для объекта дерева, представляющего корень рабочей копии. То бишь, директории alpha. Последняя строчка – комментарий коммита.



Направить текущую ветку на новый коммит


Наконец, команда коммита направляет текущую ветку на новый объект коммита.

Какая у нас текущая ветка? Git идёт в файл HEAD в .git/HEAD и видит:

ref: refs/heads/master

Это значит, что HEAD указывает на master. master – текущая ветка.

HEAD и master – это ссылки. Ссылка – это метка, используемая Git, или пользователем, для идентификации определённого коммита.

Файла, представляющего ссылку master, не существует, поскольку это первый коммит в репозитории. Git создаёт файл в .git/refs/heads/master и задаёт его содержимое – хэш объекта коммит:

74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd

(Если вы параллельно чтению вбиваете в Git команды, ваш хэш a1 будет отличаться от моего. Хэш объектов содержимого – блобов и деревьев – всегда получаются теми же. А коммитов – нет, потому что они учитывают даты и имена создателей).

Добавим в граф Git HEAD и master:



HEAD указывает на master, как и до коммита. Но теперь master существует и указывает на новый объект коммита.

Делаем не первый коммит


Ниже приведён граф после коммита a1. Включены рабочая копия и индекс.



Заметьте, что у рабочей копии, индекса и коммита одинаковое содержимое data/letter.txt и data/number.txt. Индекс и HEAD используют хэши для указания на блобы, но содержимое рабочей копии хранится в виде текста в другом месте.

~/alpha $ printf '2' > data/number.txt

Пользователь меняет содержимое data/number.txt на 2. Это обновляет рабочую копию, но оставляет индекс и HEAD без изменений.



~/alpha $ git add data/number.txt

Пользователь добавляет файл в Git. Это добавляет блоб, содержащий 2, в директорию objects. Указатель записи индекса для data/number.txt указывает на новый блоб.



~/alpha $ git commit -m 'a2'
          [master f0af7e6] a2

Пользователь делает коммит. Шаги у него такие же, как и раньше.

Во-первых, создаётся новый древовидный граф для представления содержимого индекса.

Запись в индексе для data/number.txt изменилась. Старое дерево data уже не отражает проиндексированное состояние директории data. Нужно создать новый объект-дерево data.

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt

Хэш у нового объекта отличается от старого дерева data. Нужно создать новое дерево root для записи этого хэша.

040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data

Во-вторых, создаётся новый объект коммита.

tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500

a2

Первая строка объекта коммита указывает на новый объект root. Вторая строка указывает на a1: родительский коммит. Для поиска родительского коммита Git идёт в HEAD, проходит до master и находит хэш коммита a1.

В третьих, содержимое файла-ветви master меняется на хэш нового коммита.


Коммит a2


Граф Git без рабочей копии и индекса

Свойства графа:

• Содержимое хранится в виде дерева объектов. Это значит, что в базе хранятся только изменения. Взгляните на граф сверху. Коммит а2 повторно использует блоб, сделанный до коммита а1. Точно так же, если вся директория от коммита до коммита не меняется, неё дерево и все блобы и нижележащие деревья можно использовать повторно. Обычно изменения между коммитами небольшие. Это значит, что Git может хранить большие истории коммитов, занимая немного места.

• У каждого коммита есть предок. Это значит, что в репозитории можно хранить историю проекта.

• Ссылки – входные точки для той или иной истории коммита. Это значит, что коммитам можно давать осмысленные имена. Пользователь организовывает работу в виде родословной, осмысленной для своего проекта, с ссылками типа fix-for-bug-376. Git использует символьные ссылки вроде HEAD, MERGE_HEAD и FETCH_HEAD для поддержки команды редактирования истории коммитов.

• Узлы в директории objects/ неизменны. Это значит, что содержимое редактируется, но не удаляется. Каждый кусочек содержимого, добавленный когда-либо, каждый сделанный коммит хранится где-то в директории objects.

• Ссылки изменяемы. Значит, смысл ссылки может измениться. Коммит, на который указывает master, может быть наилучшей версией проекта на текущий момент, но вскоре его может сменить новый коммит.

• Рабочая копия и коммиты, на которые указывают ссылки, доступны сразу. Другие коммиты – нет. Это значит, что недавнюю историю легче вызвать, но и меняется она чаще. Можно сказать, что память Git постоянно исчезает, и её надо стимулировать всё более жёсткими тычками.

Рабочую копию легче всего вызвать из истории, поскольку она находится в корне репозитория. Её вызов даже не требует команды Git. Но также это и самая непостоянная точка в истории. Пользователь может сделать десяток версий файла, но Git не запишет ни одну из них, пока они не добавлены.

Коммит, на который указывает HEAD, очень легко вызвать. Он находится на конце подтверждённой ветви. Чтобы просмотреть его содержимое, пользователь может просто сохранить и затем изучить рабочую копию. И в то же время, HEAD – самая часто изменяющаяся ссылка.

Коммит, на который указывает конкретная ссылка, легко вызвать. Пользователь просто подтвердит эту ветку. Конец ветки меняется реже, чем HEAD, но достаточно для того, чтобы смысл названия ветки можно было менять.

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

Подтверждение (check out) коммита


~/alpha $ git checkout 37888c2
          You are in 'detached HEAD' state...

Пользователь подтверждает коммит а2, используя его хэш. (Если вы запускаете эти команды, то данная команда у вас не сработает. Используйте git log, чтобы выяснить хэш вашего коммита а2).

Подтверждение состоит из четырёх шагов.

Во-первых, Git получает коммит а2 и граф, на который тот указывает.

Во-вторых, он делает записи о файлах в графе в рабочей копии. В результате изменений не происходит. В рабочей копии уже есть содержимое графа, поскольку HEAD уже указывал на коммит а2 через master.

В-третьих, Git делает записи о файлах в граф в индексе. Тут тоже изменений не происходит. В индексе уже содержится коммит а2.

В-четвёртых, содержимому HEAD присваивается хэш коммита а2:

f0af7e62679e144bb28c627ee3e8f7bdb235eee9

Запись хэша в HEAD приводит репозиторий в состоянии с отделённым HEAD. Обратите внимание на графе ниже, что HEAD указывает непосредственно на коммит а2, а не на master.



~/alpha $ printf '3' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a3'
          [detached HEAD 3645a0e] a3

Пользователь записывает в содержимое data/number.txt значение 3 и коммитит изменение. Git идёт к HEAD для получения родителя коммита а3. Вместо того, чтобы найти и последовать по ссылке на ветвь, он находит и возвращает хэш коммита а2.

Git обновляет HEAD, чтобы он указывал на хэш нового коммита а3. Репозиторий по-прежнему находится в состоянии с отделённым HEAD. Он не на ветви, поскольку ни один коммит не указывает ни на а3, ни на любой из его потомков. Его легко потерять.

Далее мы будем опускать деревья и блобы из диаграмм графов.



Создать ответвление (branch)

~/alpha $ git branch deputy

Пользователь создаёт новую ветвь под названием deputy. В результате появляется новый файл в .git/refs/heads/deputy, содержащий хэш, на который указывает HEAD: хэш коммита а3.

Ветви – это только ссылки, а ссылки – это только файлы. Это значит, что ветви Git весят очень немного.

Создание ветви deputy размещает новый коммит а3 на ответвлении. HEAD всё ещё отделён, поскольку всё ещё показывает непосредственно на коммит.



Подтвердить ответвление


~/alpha $ git checkout master
          Switched to branch 'master'

Пользователь подтверждает ответвление master.

Во-первых, Git получает коммит а2, на который указывает master, и получает граф, на который указывает коммит.

Во-вторых, Git вносит файловые записи в граф в файлы рабочей копии. Это меняет контент data/number.txt на 2.

В-третьих, Git вносит файловые записи в граф индекса. Это обновляет вхождение для data/number.txt на хэш блоба 2.

В-четвёртых, Git направляет HEAD на master, меняя его содержимое с хэша на

ref: refs/heads/master



Подтвердить ветвь, несовместимую с рабочей копией

~/alpha $ printf '789' > data/number.txt
~/alpha $ git checkout deputy
          Your changes to these files would be overwritten
          by checkout:
            data/number.txt
          Commit your changes or stash them before you
          switch branches.

Пользователь случайно присваивает содержимому data/number.txt значение 789. Он пытается подтвердить deputy. Git препятствует подтверждению.

HEAD указывает на master, указывающий на a2, где в data/number.txt записано 2. deputy указывает на a3, где в data/number.txt записано 3. В версии рабочей копии для data/number.txt записано 789. Все эти версии различны, и разницу нужно как-то устранить.

Git может заменить версию рабочей копии версией из подтверждаемого коммита. Но он всеми силами избегает потери данных.

Git может объединить версию рабочей копии с подтверждаемой версией. Но это сложно.

Поэтому Git отклоняет подтверждение.

~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
          Switched to branch 'deputy'

Пользователь замечает, что он случайно отредактировал data/number.txt и присваивает ему обратно 2. Затем он успешно подтверждает deputy.



Объединение с предком


~/alpha $ git merge master
          Already up-to-date.

Пользователь включает master в deputy. Объединение двух ветвей означает объединение двух коммитов. Первый коммит – тот, на который указывает deputy: принимающий. Второй коммит – тот, на который указывает master: дающий. Для объединения Git ничего не делает. Он сообщает, что он уже «Already up-to-date».

Серия объединений в графе интерпретируется как серия изменений содержимого репозитория. Это значит, что при объединении если коммит дающего получается предком коммита принимающего, Git ничего не делает. Эти изменения уже внесены.

Объединение с потомком


~/alpha $ git checkout master
          Switched to branch 'master'

Пользователь подтверждает master.



~/alpha $ git merge deputy
          Fast-forward

Он включает deputy в master. Git обнаруживает, что коммит принимающего, а2, является предком коммита дающего, а3. Он может сделать объединение с быстрой перемоткой вперёд.

Он берёт коммит дающего и граф, на который он указывает. Он вносит файловые записи в графы рабочей копии и индекса. Затем он «перематывает» master, чтобы тот указывал на а3.



Серия коммитов на графе интерпретируется как серия изменений содержимого репозитория. Это значит, что при объединении, если дающий является потомком принимающего, история не меняется. Уже существует последовательность коммитов, описывающих нужные изменения: последовательность коммитов между принимающим и дающим. Но, хотя история Git не меняется, граф Git меняется. Конкретная ссылка, на которую указывает HEAD, обновляется, чтобы указывать на коммит дающего.

Объединить два коммита из разных родословных


~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
          [master 7b7bd9a] a4

Пользователь записывает 4 в содержимое number.txt и коммитит изменение в master.

~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
          [deputy 982dffb] b3

Пользователь подтверждает deputy. Он записывает «b» в содержимое data/letter.txt и коммитит изменение в deputy.



У коммитов могут быть общие родители. Это значит, что в истории коммитов можно создавать новые родословные.

У коммитов может быть несколько родителей. Это значит, что разные родословные можно объединить коммитом с двумя родителями: объединяющим коммитом.

~/alpha $ git merge master -m 'b4'
          Merge made by the 'recursive' strategy.

Пользователь объединяет master с deputy.

Git обнаруживает, что принимающий, b3, и дающий, a4, находятся в разных родословных. Он выполняет объединяющий коммит. У этого процесса восемь шагов.

1. Git записывает хэш дающего коммита в файл alpha/.git/MERGE_HEAD. Наличие этого файла сообщает Git, что он находится в процессе объединения.

2. Во-вторых, Git находит базовый коммит: самый новый предок, общий для дающего и принимающего коммитов.



У коммитов есть родители. Это значит, что можно найти точку, в которой разделились две родословных. Git отслеживает цепочку назад от b3, чтобы найти всех его предков, и назад от a4 с той же целью. Он находит самого нового из их общих предков, а3. Это базовый коммит.

3. Git создают индексы для базового, дающего и принимающего коммита из их древовидных графов.

4. Git создаёт diff, объединяющий изменения, которые принимающий и дающий коммиты произвели с базовым. Этот diff – список путей файлов, указывающих на изменения: добавление, удаление, модификацию или конфликты.

Git получает список всех файлов, имеющихся в индексах базового коммита, получающего и дающего. Для каждого он сравнивает записи в индексах и решает, каким образом нужно менять файл. Он пишет соответствующую запись в diff. В нашем случае в diff попадают две записи.

Первая запись для data/letter.txt. Содержимое этого файла – «a» в базовом, «b» в получающем и «a» в дающем. Содержимое у базового и получающего различается. Но у базового и дающего – совпадает. Git видит, что содержимое изменено получающим, а не дающим. Запись в diff для data/letter.txt – это изменение, а не конфликт.

Вторая запись в diff – для data/number.txt. В этом случае, содержимое совпадает у базового и получающего, а у дающего оно другое. Запись в diff для data/number.txt — также изменение.

Есть возможность найти базовый коммит объединения. Это значит, что если файл отличается от базового только в получающем или дающем, Git может автоматически провести объединение файла. Это уменьшает работу, которую должен проделать пользователь.

5. Изменения, описанные записями в diff, применяются к рабочей копии. Содержимому data/letter.txt присваивается значение b, а содержимому data/number.txt – 4.

6. Изменения, описанные записями в diff, применяются к индексу. Запись, относящаяся к data/letter.txt, указывает на блоб b, а запись для data/number.txt – на блоб 4.

7. Подтверждается обновлённый индекс:

tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500

b4

Обратите внимание, что у коммита два родителя.

8. Git направляет текущую ветвь, deputy, на новый коммит.



Объединение двух коммитов из разных родословных, изменяющих один и тот же файл


~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ git merge deputy
          Fast-forward

Пользователь подтверждает master. Он объединяет deputy и master. Это приводит к перемотке master до коммита b4. Теперь master и deputy указывают на один и тот же коммит.



~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
          [deputy bd797c2] b5

Пользователь подтверждает deputy. Он устанавливает содержимое data/number.txt в 5 и подтверждает изменение в deputy.

~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
          [master 4c3ce18] b6

Пользователь подтверждает master. Он устанавливает содержимое data/number.txt в 6 и подтверждает изменение в master.



~/alpha $ git merge deputy
          CONFLICT in data/number.txt
          Automatic merge failed; fix conflicts and
          commit the result.

Пользователь объединяет deputy и master. Выявляется конфликт и объединение приостанавливается. Процесс объединения с конфликтом делает те же шесть первых шагов, что и процесс объединения без конфликта: определить .git/MERGE_HEAD, найти базовый коммит, создать индексы для базового, принимающего и дающего коммитов, создать diff, обновить рабочую копию и обновить индекс. Из-за конфликта седьмой шаг с коммитом и восьмой с обновлением ссылки не выполняются. Пройдём по ним заново и посмотрим, что получается.

1. Git записывает хэш дающего коммита в файл .git/MERGE_HEAD.



2. Git находит базовый коммит, b4.

3. Git генерирует индексы для базового, принимающего и дающего коммитов.

4. Git генерирует diff, комбинирующий изменения базового коммита, произведённые получающим и дающим коммитами. Этот diff – список путей к файлам, указывающим на изменения: добавление, удаление, модификацию или конфликт.

В данном случае diff содержит только одну запись: data/number.txt. Она отмечена как конфликт, поскольку содержимое data/number.txt отличается в дающем, принимающем и базовом коммитах.

5. Изменения, определяемые записями в diff, применяются к рабочей копии. В области конфликта Git выводит обе версии в файл рабочей копии. Содержимое файла ata/number.txt становится следующим:

<<<<<<< HEAD
6
=======
5
>>>>>>> deputy

6. Изменения, определённые записями в diff, применяются к индексу. Записи в индексе имеют уникальный идентификатор в виде комбинации пути к файлу и этапа. У записи файла без конфликта этап равен 0. Перед объединением индекс выглядел так, где нули – это номера этапов:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb

После записи diff в индекс тот выглядит уже так:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61

Запись data/letter.txt на этапе 0 такая же, какая и была до объединения. Запись для data/number.txt с этапом 0 исчезла. Вместо неё появились три новых. У записи с этапом 1 хэш от базового содержимого data/number.txt. У записи с этапом 3 хэш от содержимого дающего data/number.txt. Присутствие трёх записей говорит Git, что для data/number.txt возник конфликт.

Объединение приостанавливается.

~/alpha $ printf '11' > data/number.txt
~/alpha $ git add data/number.txt

Пользователь интегрирует содержимое двух конфликтующих версий, устанавливая содержимое data/number.txt равным 11. Он добавляет файл в индекс. Git добавляет блоб, содержащий 11. Добавление конфликтного файла говорит Git о том, что конфликт решён. Git удаляет вхождения data/number.txt для этапов 1, 2 и 3 из индекса. Он добавляет запись для data/number.txt на этапе 0 с хэшем от нового блоба. Теперь индекс содержит записи:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503

~/alpha $ git commit -m 'b11'
          [master 251a513] b11

7. Пользователь коммитит. Git видит в репозитории .git/MERGE_HEAD, что говорит ему о том, что объединение находится в процессе. Он проверяет индекс и не обнаруживает конфликтов. Он создаёт новый коммит, b11, для записи содержимого разрешённого объединения. Он удаляет файл .git/MERGE_HEAD. Это и завершает объединение.

8. Git направляет текущую ветвь, master, на новый коммит.



Удаление файла


Диаграмма для графа Git включает историю коммитов, деревья и блобы последнего коммита, рабочую копию и индекс:



~/alpha $ git rm data/letter.txt
          rm 'data/letter.txt'

Пользователь указывает Git удалить data/letter.txt. Файл удаляется из рабочей копии. Из индекса удаляется запись.



~/alpha $ git commit -m '11'
          [master d14c7d2] 11

Пользователь делает коммит. Как обычно при коммите, Git строит граф, представляющий содержимое индекса. data/letter.txt не включён в граф, поскольку его нет в индексе.



Копировать репозиторий


~/alpha $ cd ..
      ~ $ cp -R alpha bravo

Пользователь копирует содержимое репозитория alpha/ в директорию bravo/. Это приводит к следующей структуре:

~
├── alpha
|   └── data
|       └── number.txt
└── bravo
    └── data
        └── number.txt

Теперь в директории bravo есть новый граф Git:



Связать репозиторий с другим репозиторием


      ~ $ cd alpha
~/alpha $ git remote add bravo ../bravo

Пользователь возвращается в репозиторий alpha. Он назначает bravo удалённым репозиторием для alpha. Это добавляет несколько строк в файл alpha/.git/config:

[remote "bravo"]
	url = ../bravo/

Строки говорят, что существует удалённый репозиторий bravo в директории ../bravo.

Получить ветвь с удалённого репозитория


~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
          [master 94cd04d] 12

Пользователь переходит к репозиторию bravo. Он присваивает содержимому data/number.txt значение 12 и коммитит изменение в master на bravo.



~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
          Unpacking objects: 100%
          From ../bravo
            * branch master -> FETCH_HEAD

Пользователь переходит в репозиторий alpha. Он копирует master из bravo в alpha. У этого процесса четыре шага.

1. Git получает хэш коммита, на который в bravo указывает master. Это хэш коммита 12.

2. Git составляет список всех объектов, от которых зависит коммит 12: сам объект коммита, объекты в его графе, коммиты предка коммита 12, и объекты из их графов. Он удаляет из этого списка все объекты, которые уже есть в базе alpha. Остальное он копирует в alpha/.git/objects/.

3. Содержимому конкретного файла ссылки в alpha/.git/refs/remotes/bravo/master присваивается хэш коммита 12.

4. Содержимое alpha/.git/FETCH_HEAD становится следующим:

94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo

Это значит, что самая последняя команда fetch достала коммит 12 из master с bravo.



Объекты можно копировать. Это значит, что у разных репозиториев может быть общая история.

Репозитории могут хранить ссылки на удалённые ветви типа alpha/.git/refs/remotes/bravo/master. Это значит, что репозиторий может локально записывать состояние ветви удалённого репозитория. Он корректен во время его копирования, но станет устаревшим в случае изменения удалённой ветви.

Объединение FETCH_HEAD


~/alpha $ git merge FETCH_HEAD
          Updating d14c7d2..94cd04d
          Fast-forward

Пользователь объединяет FETCH_HEAD. FETCH_HEAD – это просто ещё одна ссылка. Она указывает на коммит 12, дающий. HEAD указывает на коммит 11, принимающий. Git делает объединение-перемотку и направляет master на коммит 12.



Получить ветвь удалённого репозитория


~/alpha $ git pull bravo master
          Already up-to-date.

Пользователь переносит master из bravo в alpha. Pull – это сокращение для «скопировать и объединить FETCH_HEAD». Git выполняет обе команды и сообщает, что master «Already up-to-date».

Клонировать репозиторий


~/alpha $ cd ..
      ~ $ git clone alpha charlie
          Cloning into 'charlie'

Пользователь переезжает в верхний каталог. Он клонирует alpha в charlie. Клонирование приводит к результатам, сходным с командой cp, которую пользователь выполнил для создания репозитория bravo. Git создаёт новую директорию charlie. Он инициализирует charlie в качестве репозитория, добавляет alpha в качестве удалённого под именем origin, получает origin и объединяет FETCH_HEAD.

Разместить (push) ветвь на подтверждённой ветви на удалённом репозитории


      ~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
          [master 3238468] 13

Пользователь возвращается в репозиторий alpha. Присваивает содержимому data/number.txt значение 13 и коммитит изменение в master на alpha.

~/alpha $ git remote add charlie ../charlie

Он назначает charlie удалённым репозиторием alpha.

~/alpha $ git push charlie master
          Writing objects: 100%
          remote error: refusing to update checked out
          branch: refs/heads/master because it will make
          the index and work tree inconsistent

Он размещает master на charlie. Все объекты, необходимые для коммита 13, копируются в charlie.

В этот момент процесс размещения останавливается. Git, как всегда, сообщает пользователю, что пошло не так. Он отказывается размещать на ветви, которая подтверждена удалённо. Это имеет смысл. Размещение обновило бы удалённый индекс и HEAD. Это привело бы к путанице, если бы кто-то ещё редактировал рабочую копию на удалённом репозитории.

В этот момент пользователь может создать новую ветвь, объединить коммит 13 с ней и разместить эту ветвь на charlie. Но пользователям требуется репозиторий, на котором можно размещать, что угодно. Им нужен центральный репозиторий, на котором можно размещать, и с которого можно получать (pull), но на который напрямую никто не коммитит. Им нужно нечто типа удалённого GitHub. Им нужен чистый (bare) репозиторий.

Клонируем чистый (bare) репозиторий


~/alpha $ cd ..
      ~ $ git clone alpha delta --bare
          Cloning into bare repository 'delta'

Пользователь переходит в верхнюю директорию. Он создаёт клон delta в качестве чистого репозитория. Это обычный клон с двумя особенностями. Файл config говорит о том, что репозиторий чистый. И файлы, обычно хранящиеся в директории .git, хранятся в корне репозитория:

delta
├── HEAD
├── config
├── objects
└── refs



Разместить ветвь в чистом репозитории


      ~ $ cd alpha
~/alpha $ git remote add delta ../delta

Пользователь возвращается в репозиторий alpha. Он назначает delta удалённым репозиторием для alpha.

~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
          [master cb51da8] 14

Он присваивает содержимому значение 14 и коммитит изменение в master на alpha.



~/alpha $ git push delta master
          Writing objects: 100%
          To ../delta
            3238468..cb51da8 master -> master

Он размещает master в delta. Размещение проходит в три этапа.

1. Все объекты, необходимые для коммита 14 на ветви master, копируются из alpha/.git/objects/ в delta/objects/.

2. delta/refs/heads/master обновляется до коммита 14.

3. alpha/.git/refs/remotes/delta/master направляют на коммит 14. В alpha находится актуальная запись состояния delta.


Итог


Git построен на графе. Почти все команды Git манипулируют этим графом. Чтобы понять Git, сконцентрируйтесь на свойствах графа, а не процедурах или командах.

Чтобы узнать о Git ещё больше, изучайте директорию .git. Это не страшно. Загляните вовнутрь. Измените содержимое файлов и посмотрите, что произойдёт. Создайте коммит вручную. Попробуйте посмотреть, как сильно вы сможете сломать репозиторий. Затем почините его.
Support the author
Share post

Similar posts

Comments 38

    +6

    Имхо, не лучшее описание темы. В pro git написано сильно понятнее и целостнее. Вся десятая глава про это: https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain

      0
      Спасибо за подсказку, я, например, не знал этой книжки. У данной статьи есть одно довольно важное преимущество перед книжкой – она достаточно короткая, но при этом ёмкая.
        +2

        Книжку рекомендую прочитать полностью. Наступает просветление, понимание того, как гит устроен, что в нем можно сделать, а что нельзя. Еще приходит осознание, какие операции быстрые, а какие тормозные (многим это не актуально, но если работать с гигантскими репозиториями, это очень полезные знания).

          0
          У меня гигантских репозиториев нет (пока?), но тема в любом случае интересная. Спасибо за совет!
            +6

            У меня клон основного рабочего репозитория занимает порядка пяти гигабайт, с чекаутом получается в районе десяти (не говоря уже, что со всеми артефактами сборки около 70) И там тормоза уже сильно заметны. Если интересно, могу рассказать подробнее про медленные/быстрые команды git, но это скорее всего тянет на отдельный пост.

              +3
              Расскажите отдельным постом. Я уверен, что это будет многим интересно, а в комментариях к другому посту может потеряться.
                0
                Я когда-то переводил статью коллеги о монолитных репозиториях, в которой освещалась, в частности, проблема производительности. Впрочем, там, скорее, более общие слова, а не детальный анализ, так что еще одна статья на тему будет интересной.
                  0
                  Спасибо за ссылку!
            +1
            Она, оказывается, ещё и на русском (и не только) есть. Если кому-то удобнее читать по-русски, вот ссылка:
            https://git-scm.com/book/ru/v1
            Единственный момент – по-русски можно прочитать перевод только первого издания книги, а на английском (и французском) опубликовано и второе издание.
              +1
                0
                Там ещё не всё переведено (даже по подзаголовкам видно, что некоторые до сих пор на английском).
        +8

        Первый раз вижу перевод checkout как "подтверждение"… Даже гуглопереводчик дает более адекватный вариант "выписывать(ся)".

          +2
          В этом контексте я бы перевёл checkout как «переключиться».
          «Переключиться на коммит», «Переключиться на ветку master», и т.д.
            0
            Или просто «взять» — например, как во фразе «взять книги в библиотеке».
              –1
              Лучше совсем не переводить термины предметной области. И читать невозможно и общаться с теми, кто будет употреблять перевод тяжело.
                +2

                Untranslated термин предметной area выглядеть как marketing булшит.

              +2
              В свое время для понимания веток в git прошел вот этот замечательный курс:
              http://learngitbranching.js.org/
                +2
                Спасибо за интересную статью!

                А есть ли что-нибудь похожее по Mercurial?
                  +2
                  Там и так всё понятно, по крайней мере с TortoiseHg я никаких проблем не испытывал. ИМХО, лучший графический клиент для систем контроля версий, я его даже для git использую.
                    0

                    А как вы его для git используете О_о? Он же сильно завязан на фишки Mercurial.

                      +1
                      Плагин hg-git. Вполне нормально пашет, кроме совсем сложных случаев с подрепозиториями. По умолчанию ветки из git становятся анонимными, но в конфиге можно их привязать, сделав настоящими.
                      Проблемы могут быть разве что если начать править историю в git (или hg через соответствующие плагины), но это и в среде git считается плохой практикой после опубликования коммитов.
                        +1

                        В рассылке вроде как evolve один юзер писал, как вполне себе правил историю и потом публиковал её в git :)

                          0
                          У меня при заливке правленных коммитов на гитхаб была проблема, не поддерживался ключ -f, который нужен для перезаписи истории, пришлось выкачивать на веб-сервере, удалять исправленные коммиты, и пушить оттуда. Только тогда заработало.
                            –2

                            емнип, для публикации правленой истории в репозиторий git ключ -f не нужен, только закладку обновить при необходимости :)

                              +1

                              Напротив, ключ -f в команде push нужен исключительно для публикации правленной истории.

                  0

                  А чем вас «Mercurial: The Definitive Guide» (и русский перевод книги) не устраивает? В частности, глава «За кулисами» полностью посвящена внутренней кухне Mercurial.

                  0
                    +2
                    Отличная статья. Спасибо! Терпеть не могу совершать «магические пассы», не понимая суть, а в случае с git лично у меня так и было. Эта статья позволяет понять, что именно происходит при использовании этих команд, и почему это происходит. Поддержу предыдущего комментатора – было бы здорово почитать подобный материал и про Mercurial.
                      0
                      Файл letter.txt путешествует между папками alpha и data (он то там, то там)
                        0

                        Он всегда в data. А data — в alpha (а также в bravo)

                        +1
                        Очень запутанное объяснение, имхо. Несмотря на то, что работаю с git около 2х лет, но многие вещи из статьи так и остались не понятными для меня.
                          0

                          В статье нигде не упомянуто про git cat-file, а было бы очень полезно хотя бы в начале продемонстрировать что и как смотреть. Например, файл index — он же бинарный, его нельзя просто открыть текстовым редактором и увидеть строки, которые показывает автор статьи.

                            0

                            А как через cat-file открыть .git/index?
                            Для просмотра индекса можно использовать "git ls-files --stage" (который тоже забыли упомянуть).

                            +1

                            Термины конечно в статье порой просто глаз колят.

                              0
                              Спасибо! Нужная статья для объяснения внутреннего устройства git
                              А для более высокоуровневого понимания команд полезна также "Наглядная справка по git"

                              Only users with full accounts can post comments. Log in, please.