что такое индекс git
Что такое индекс git
Замечание, в старой документации вы возможно встретите определение индекса «current directory cache(кэш текущей директории)» или просто «cache(кэш)». Он имеет три важдых свойства:
Индекс содержит всю информацию необходимую чтобы сгенерировать один (уникальный) объект дерево.
Например, выполнение git commit сгенерирует этот объект дерево из индекса, сохранит его в базе данных объектов, и использует его как объект дерево связанный с новым коммитом.
Индекс разрешает быстрое сравнение между объектом дерево которое он определяет и рабочим деревом.
Он делает это сохраняя некоторую дополнительную информацию каждой записи (такой как время последней модификации). Эти данные не отображаются выше, и не сохраняются в созданном объекте дерево, но они могут быть использованы чтобы быстро определить какие файлы в рабочей директории отличаются от тех что в индексе, и поэтому предохраняет git от постоянного чтения всех данных из таких файлов в поисках изменений.
Он может эффективно предствить информацию о конфликтах при слиянии между различными объектами дерево, позволяя каждому пути быть связанным с достаточной информацией о вовлеченных деревьях, и вы можете создавать слияние между ними в три способа.
В процессе слияния, индекс может хранить множество версий одного файла (называемых «stages(заморозка)»). Третья колонка в выводе git ls-files выше это номер заморозки, и примет значения отличные от 0 для файлов с конфликтами при слиянии.
Индекс это своего рода временная область заморозки, которая заполняется деревом над котором вы в процессе работы.
Как работает Git
В этом эссе описана схема работы Git. Предполагается, что вы знакомы с Git достаточно, чтобы использовать его для контроля версий своих проектов.
Эссе концентрируется на структуре графа, на которой основан Git, и на том, как свойства этого графа определяют поведение Git. Изучая основы, вы строите своё представление на достоверной информации, а не на гипотезах, полученных из экспериментов с API. Правильная модель позволит вам лучше понять, что сделал Git, что он делает и что он собирается сделать.
Текст разбит на серии команд, работающих с единым проектом. Иногда встречаются наблюдения по поводу структуры данных графа, лежащего в основе Git. Наблюдения иллюстрируют свойство графа и поведение, основанное на нём.
После прочтения для ещё более глубокого погружения можно обратиться к обильно комментируемому исходному коду моей реализации Git на JavaScript.
Создание проекта
Пользователь создаёт директорию alpha
Он перемещается в эту директорию и создаёт директорию data. Внутри он создаёт файл letter.txt с содержимым «а». Директория alpha выглядит так:
Инициализируем репозиторий
Теперь директория alpha выглядит так:
Добавляем файлы
Пользователь запускает git add на data/letter.txt. Происходят две вещи.
Заметьте, что простое добавление файла в Git приводит к сохранению его содержимого в директории objects. Оно будет храниться там, если пользователь удалит data/letter.txt из рабочей копии.
Пользователь создаёт файл data/number.txt содержащий 1234.
Рабочая копия выглядит так:
Пользователь добавляет файл в Git.
Команда git add создаёт блоб-файл, в котором хранится содержимое data/number.txt. Он добавляет в индекс запись для data/number.txt, указывающую на блоб. Вот содержимое индекса после повторного запуска git add:
Заметьте, что в индексе перечислены только файлы из директории data, хотя пользователь давал команду git add data. Сама директория data отдельно не указывается.
Когда пользователь создал data/number.txt, он хотел написать 1, а не 1234. Он вносит изменение и снова добавляет файл к индексу. Эта команда создаёт новый блоб с новым содержимым. Она обновляет запись в индексе для data/number.txt с указанием на новый блоб.
Делаем коммит
Пользователь делает коммит a1. Git выводит данные о нём. Мы вскоре объясним их.
У команды commit есть три шага. Она создаёт граф, представляющий содержимое версии проекта, которую коммитят. Она создаёт объект коммита. Она направляет текущую ветку на новый объект коммита.
Создание графа
Git запоминает текущее состояние проекта, создавая древовидный граф из индекса. Этот граф записывает расположение и содержимое каждого файла в проекте.
Граф состоит из двух типов объектов: блобы и деревья. Блобы сохраняются через git add. Они представляют содержимое файлов. Деревья сохраняются при коммите. Дерево представляет директорию в рабочей копии. Ниже приведён объект дерева, записавший содержимое директории data при новом коммите.
Первая строка записывает всё необходимое для воспроизводства data/letter.txt. Первая часть хранит права доступа к файлу. Вторая – что содержимое файла хранится в блобе, а не в дереве. Третья – хэш блоба. Четвёртая – имя файла.
Вторая строчка тем же образом относится к data/number.txt.
Ниже указан объект-дерево для alpha, корневой директории проекта:
Единственная строка указывает на дерево data.
В приведённом графе дерево root указывает на дерево data. Дерево data указывает на блобы для data/letter.txt и data/number.txt.
Создание объекта коммита
Первая строка указывает на дерево графа. Хэш – для объекта дерева, представляющего корень рабочей копии. То бишь, директории alpha. Последняя строчка – комментарий коммита.
Направить текущую ветку на новый коммит
Наконец, команда коммита направляет текущую ветку на новый объект коммита.
Это значит, что HEAD указывает на master. master – текущая ветка.
HEAD и master – это ссылки. Ссылка – это метка, используемая Git, или пользователем, для идентификации определённого коммита.
(Если вы параллельно чтению вбиваете в Git команды, ваш хэш a1 будет отличаться от моего. Хэш объектов содержимого – блобов и деревьев – всегда получаются теми же. А коммитов – нет, потому что они учитывают даты и имена создателей).
Добавим в граф Git HEAD и master:
HEAD указывает на master, как и до коммита. Но теперь master существует и указывает на новый объект коммита.
Делаем не первый коммит
Ниже приведён граф после коммита a1. Включены рабочая копия и индекс.
Заметьте, что у рабочей копии, индекса и коммита одинаковое содержимое data/letter.txt и data/number.txt. Индекс и HEAD используют хэши для указания на блобы, но содержимое рабочей копии хранится в виде текста в другом месте.
Пользователь меняет содержимое data/number.txt на 2. Это обновляет рабочую копию, но оставляет индекс и HEAD без изменений.
Пользователь добавляет файл в Git. Это добавляет блоб, содержащий 2, в директорию objects. Указатель записи индекса для data/number.txt указывает на новый блоб.
Пользователь делает коммит. Шаги у него такие же, как и раньше.
Во-первых, создаётся новый древовидный граф для представления содержимого индекса.
Запись в индексе для data/number.txt изменилась. Старое дерево data уже не отражает проиндексированное состояние директории data. Нужно создать новый объект-дерево data.
Хэш у нового объекта отличается от старого дерева data. Нужно создать новое дерево root для записи этого хэша.
Во-вторых, создаётся новый объект коммита.
Первая строка объекта коммита указывает на новый объект root. Вторая строка указывает на a1: родительский коммит. Для поиска родительского коммита Git идёт в HEAD, проходит до master и находит хэш коммита a1.
В третьих, содержимое файла-ветви master меняется на хэш нового коммита.
Граф 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) коммита
Пользователь подтверждает коммит а2, используя его хэш. (Если вы запускаете эти команды, то данная команда у вас не сработает. Используйте git log, чтобы выяснить хэш вашего коммита а2).
Подтверждение состоит из четырёх шагов.
Во-первых, Git получает коммит а2 и граф, на который тот указывает.
Во-вторых, он делает записи о файлах в графе в рабочей копии. В результате изменений не происходит. В рабочей копии уже есть содержимое графа, поскольку HEAD уже указывал на коммит а2 через master.
В-третьих, Git делает записи о файлах в граф в индексе. Тут тоже изменений не происходит. В индексе уже содержится коммит а2.
В-четвёртых, содержимому HEAD присваивается хэш коммита а2:
Запись хэша в HEAD приводит репозиторий в состоянии с отделённым HEAD. Обратите внимание на графе ниже, что HEAD указывает непосредственно на коммит а2, а не на master.
Пользователь записывает в содержимое data/number.txt значение 3 и коммитит изменение. Git идёт к HEAD для получения родителя коммита а3. Вместо того, чтобы найти и последовать по ссылке на ветвь, он находит и возвращает хэш коммита а2.
Git обновляет HEAD, чтобы он указывал на хэш нового коммита а3. Репозиторий по-прежнему находится в состоянии с отделённым HEAD. Он не на ветви, поскольку ни один коммит не указывает ни на а3, ни на любой из его потомков. Его легко потерять.
Далее мы будем опускать деревья и блобы из диаграмм графов.
Создать ответвление (branch)
Ветви – это только ссылки, а ссылки – это только файлы. Это значит, что ветви Git весят очень немного.
Создание ветви deputy размещает новый коммит а3 на ответвлении. HEAD всё ещё отделён, поскольку всё ещё показывает непосредственно на коммит.
Подтвердить ответвление
Пользователь подтверждает ответвление master.
Во-первых, Git получает коммит а2, на который указывает master, и получает граф, на который указывает коммит.
Во-вторых, Git вносит файловые записи в граф в файлы рабочей копии. Это меняет контент data/number.txt на 2.
В-третьих, Git вносит файловые записи в граф индекса. Это обновляет вхождение для data/number.txt на хэш блоба 2.
В-четвёртых, Git направляет HEAD на master, меняя его содержимое с хэша на
Подтвердить ветвь, несовместимую с рабочей копией
Пользователь случайно присваивает содержимому 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 отклоняет подтверждение.
Пользователь замечает, что он случайно отредактировал data/number.txt и присваивает ему обратно 2. Затем он успешно подтверждает deputy.
Объединение с предком
Пользователь включает master в deputy. Объединение двух ветвей означает объединение двух коммитов. Первый коммит – тот, на который указывает deputy: принимающий. Второй коммит – тот, на который указывает master: дающий. Для объединения Git ничего не делает. Он сообщает, что он уже «Already up-to-date».
Серия объединений в графе интерпретируется как серия изменений содержимого репозитория. Это значит, что при объединении если коммит дающего получается предком коммита принимающего, Git ничего не делает. Эти изменения уже внесены.
Объединение с потомком
Пользователь подтверждает master.
Он включает deputy в master. Git обнаруживает, что коммит принимающего, а2, является предком коммита дающего, а3. Он может сделать объединение с быстрой перемоткой вперёд.
Он берёт коммит дающего и граф, на который он указывает. Он вносит файловые записи в графы рабочей копии и индекса. Затем он «перематывает» master, чтобы тот указывал на а3.
Серия коммитов на графе интерпретируется как серия изменений содержимого репозитория. Это значит, что при объединении, если дающий является потомком принимающего, история не меняется. Уже существует последовательность коммитов, описывающих нужные изменения: последовательность коммитов между принимающим и дающим. Но, хотя история Git не меняется, граф Git меняется. Конкретная ссылка, на которую указывает HEAD, обновляется, чтобы указывать на коммит дающего.
Объединить два коммита из разных родословных
Пользователь записывает 4 в содержимое number.txt и коммитит изменение в master.
Пользователь подтверждает deputy. Он записывает «b» в содержимое data/letter.txt и коммитит изменение в deputy.
У коммитов могут быть общие родители. Это значит, что в истории коммитов можно создавать новые родословные.
У коммитов может быть несколько родителей. Это значит, что разные родословные можно объединить коммитом с двумя родителями: объединяющим коммитом.
Пользователь объединяет 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. Подтверждается обновлённый индекс:
Обратите внимание, что у коммита два родителя.
8. Git направляет текущую ветвь, deputy, на новый коммит.
Объединение двух коммитов из разных родословных, изменяющих один и тот же файл
Пользователь подтверждает master. Он объединяет deputy и master. Это приводит к перемотке master до коммита b4. Теперь master и deputy указывают на один и тот же коммит.
Пользователь подтверждает deputy. Он устанавливает содержимое data/number.txt в 5 и подтверждает изменение в deputy.
Пользователь подтверждает master. Он устанавливает содержимое data/number.txt в 6 и подтверждает изменение в master.
2. Git находит базовый коммит, b4.
3. Git генерирует индексы для базового, принимающего и дающего коммитов.
4. Git генерирует diff, комбинирующий изменения базового коммита, произведённые получающим и дающим коммитами. Этот diff – список путей к файлам, указывающим на изменения: добавление, удаление, модификацию или конфликт.
В данном случае diff содержит только одну запись: data/number.txt. Она отмечена как конфликт, поскольку содержимое data/number.txt отличается в дающем, принимающем и базовом коммитах.
5. Изменения, определяемые записями в diff, применяются к рабочей копии. В области конфликта Git выводит обе версии в файл рабочей копии. Содержимое файла ata/number.txt становится следующим:
6. Изменения, определённые записями в diff, применяются к индексу. Записи в индексе имеют уникальный идентификатор в виде комбинации пути к файлу и этапа. У записи файла без конфликта этап равен 0. Перед объединением индекс выглядел так, где нули – это номера этапов:
После записи diff в индекс тот выглядит уже так:
Запись data/letter.txt на этапе 0 такая же, какая и была до объединения. Запись для data/number.txt с этапом 0 исчезла. Вместо неё появились три новых. У записи с этапом 1 хэш от базового содержимого data/number.txt. У записи с этапом 3 хэш от содержимого дающего data/number.txt. Присутствие трёх записей говорит Git, что для data/number.txt возник конфликт.
Пользователь интегрирует содержимое двух конфликтующих версий, устанавливая содержимое data/number.txt равным 11. Он добавляет файл в индекс. Git добавляет блоб, содержащий 11. Добавление конфликтного файла говорит Git о том, что конфликт решён. Git удаляет вхождения data/number.txt для этапов 1, 2 и 3 из индекса. Он добавляет запись для data/number.txt на этапе 0 с хэшем от нового блоба. Теперь индекс содержит записи:
8. Git направляет текущую ветвь, master, на новый коммит.
Удаление файла
Диаграмма для графа Git включает историю коммитов, деревья и блобы последнего коммита, рабочую копию и индекс:
Пользователь указывает Git удалить data/letter.txt. Файл удаляется из рабочей копии. Из индекса удаляется запись.
Пользователь делает коммит. Как обычно при коммите, Git строит граф, представляющий содержимое индекса. data/letter.txt не включён в граф, поскольку его нет в индексе.
Копировать репозиторий
Пользователь копирует содержимое репозитория alpha/ в директорию bravo/. Это приводит к следующей структуре:
Теперь в директории bravo есть новый граф Git:
Связать репозиторий с другим репозиторием
Пользователь возвращается в репозиторий alpha. Он назначает bravo удалённым репозиторием для alpha. Это добавляет несколько строк в файл alpha/.git/config:
Получить ветвь с удалённого репозитория
Пользователь переходит к репозиторию bravo. Он присваивает содержимому data/number.txt значение 12 и коммитит изменение в master на bravo.
Пользователь переходит в репозиторий 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 становится следующим:
Это значит, что самая последняя команда fetch достала коммит 12 из master с bravo.
Объекты можно копировать. Это значит, что у разных репозиториев может быть общая история.
Репозитории могут хранить ссылки на удалённые ветви типа alpha/.git/refs/remotes/bravo/master. Это значит, что репозиторий может локально записывать состояние ветви удалённого репозитория. Он корректен во время его копирования, но станет устаревшим в случае изменения удалённой ветви.
Объединение FETCH_HEAD
Пользователь объединяет FETCH_HEAD. FETCH_HEAD – это просто ещё одна ссылка. Она указывает на коммит 12, дающий. HEAD указывает на коммит 11, принимающий. Git делает объединение-перемотку и направляет master на коммит 12.
Получить ветвь удалённого репозитория
Пользователь переносит master из bravo в alpha. Pull – это сокращение для «скопировать и объединить FETCH_HEAD». Git выполняет обе команды и сообщает, что master «Already up-to-date».
Клонировать репозиторий
Пользователь переезжает в верхний каталог. Он клонирует alpha в charlie. Клонирование приводит к результатам, сходным с командой cp, которую пользователь выполнил для создания репозитория bravo. Git создаёт новую директорию charlie. Он инициализирует charlie в качестве репозитория, добавляет alpha в качестве удалённого под именем origin, получает origin и объединяет FETCH_HEAD.
Разместить (push) ветвь на подтверждённой ветви на удалённом репозитории
Пользователь возвращается в репозиторий alpha. Присваивает содержимому data/number.txt значение 13 и коммитит изменение в master на alpha.
Он назначает charlie удалённым репозиторием alpha.
Он размещает master на charlie. Все объекты, необходимые для коммита 13, копируются в charlie.
В этот момент процесс размещения останавливается. Git, как всегда, сообщает пользователю, что пошло не так. Он отказывается размещать на ветви, которая подтверждена удалённо. Это имеет смысл. Размещение обновило бы удалённый индекс и HEAD. Это привело бы к путанице, если бы кто-то ещё редактировал рабочую копию на удалённом репозитории.
В этот момент пользователь может создать новую ветвь, объединить коммит 13 с ней и разместить эту ветвь на charlie. Но пользователям требуется репозиторий, на котором можно размещать, что угодно. Им нужен центральный репозиторий, на котором можно размещать, и с которого можно получать (pull), но на который напрямую никто не коммитит. Им нужно нечто типа удалённого GitHub. Им нужен чистый (bare) репозиторий.
Клонируем чистый (bare) репозиторий
Разместить ветвь в чистом репозитории
Пользователь возвращается в репозиторий alpha. Он назначает delta удалённым репозиторием для alpha.
Он присваивает содержимому значение 14 и коммитит изменение в master на alpha.
Он размещает 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, сконцентрируйтесь на свойствах графа, а не процедурах или командах.