Привет, Хабр! Представляю вашему вниманию перевод статьи "Git for Computer Scientists" автора Tommi Virtanen.
GIT изнутри: введение
От себя: Периодически почитываю статьи, как различные популярные технологии устроены под капотом, наткнулся вот на этот материал. Статья показалась интересной наличием простых и понятных схем, которые воспринимаются значительно лучше, нежели простыни унылого текста. Решил перевести на русский язык. Изображения взяты из оригинала.
Кому будет интересно и, возможно, полезно: людям, ежедневно работающим с Git (т.е. каждому второму, если не первому разработчику ПО), и желающим лучше понять механизм его работы.
Примечание: для лучшего понимания статьи следует иметь представление о таком звере, как направленный ациклический граф (directed acyclic graph, DAG).
Хранилище объектов
Хранилище объектов Git, представляет собой, грубо говоря, DAG, содержащий различные типы объектов. Объекты хранятся в сжатом виде и идентифицируются SHA-1 хэшами (это хэш НЕ содержимого файла, который представляет объект, а самого его представления в Git).
Blob
Blob — это простейший объект, просто набор байтов. Это может быть файл, символическая ссылка или что угодно. Семантику определяет объект, который на этот blob указывает.
Tree (дерево)
Объекты типа "дерево" описывают каталоги (директории). Они могут указывать на blob-ы, которые хранят содержимое файлов, а также на другие деревья, создавая таким образом структуру подкаталогов.
Если узел указывает на другой узел в DAG, говорят, что он зависит от этого узла, т.е. не может существовать без него. Узлы, на которые никто не указывает, могут быть удалены с помощью сборки мусора (команда git gc), либо восстановлены с помощью команды git fsck --lost-found.
Commit
Commit ссылается на дерево, представляющее собой состояние файлов в Git в момент, когда commit был создан. Также commit может ссылаться на другие commit-ы, являющиеся его родителями:
- Если у commit-а больше 1 родителя, это означает, что он описывает операцию merge (слияние)
- Если у commit-а нет родителей, это так называемый initial (начальный) commit (т.е. первый в репозитории)
- Также возможны случаи, когда в репозитории более 1 initial commit-а — это обычно означает слияние двух отдельных репозиториев
Телом объекта commit является commit message (сообщение).
Refs (ссылки)
Ссылки (или заголовки, или ветки) похожи на стикеры с заметками, наклеенные на узлы DAG, своего рода заметки, или закладки — "я работаю тут". В отличие, от узлов DAG, которые не могут быть изменены, и могут лишь добавляться, ссылки можно перемещать как угодно. Они не хранятся в истории и не переносятся непосредственно между репозиториями.
Команда git commit добавляет новый узел в DAG и перемещает на него закладку для текущей ветки.
Ссылки находятся в пространстве имен heads/branchname, но часть heads можно опустить.
Особняком стоит ссылка HEAD — она указывает не на узел, а на другую ссылку — это указатель на текущую активную ветку.
Remote refs
Это, грубо говоря, стикеры другого цвета. Разница состоит в том, что remote ссылки находятся в другом пространстве имен, а также управляются удаленным сервером. Для их обновления используется команда git fetch.
Tag (тэг)
Тэг — это сочетание узла DAG и стикера (еще одного цвета). Тэг указывает на commit, и включает необязательное сообщение и GPG-подпись. Стикер (ссылка) является простым способом доступа к тэгу, и в случае потери, может быть восстановлен командой git fsck --lost-found.
Таким образом, Git-репозиторий — это сочетание DAG и ссылок.
История
Теперь, зная, как Git хранит историю версий, попробуем изобразить различные операции, а также понять, чем Git отличается от систем, представляющих историю, как линейные изменения для каждой ветки.
Простейший репозиторий. Мы просто скопировали (git clone) удаленный репозиторий с единственным коммитом.
Здесь мы считали (git fetch) удаленный репозиторий и получили 1 новый commit, но еще не слили его с нашей веткой.
Вот что получится после выполнения команды git merge remotes/MYSERVER/master. Так как слияние выполнено как fast forward (в нашей локальной ветке не было локальных commit-ов), произошло следующее: изменились файлы нашей рабочей копии, а также переместился указатель на ветку.
Выполним локально git commit, а затем git fetch. Теперь у нас есть и локальный и удаленный commit. Очевидно, нужен merge.
Это результат выполнения команды git merge remotes/MYSERVER/master. Поскольку у нас был локальный commit, это уж не fast forward, и в DAG создается отдельный commit для этого merge. Заметьте — у него 2 родительских commit-а.
Вот как будет выглядеть наше дерево после нескольких commit-ов, в обе ветки (локальную и удаленную) + merge. Хорошо видно, как Git DAG фиксирует всю историю наших действий.
Однако, такую историю читать сложно. Если вы еще не опубликовали свою ветку, или договорились с другими членами команды, что они не должны отталкиваться от нее в своей работе, у вас есть альтернатива: вы можете выполнить rebase для своей ветки. В этом случае ваш commit заменяется другим commit-ом, с другим родителем, на который перемещается также ссылка на ветку.
При этом ваши старые commit-ы останутся в DAG, пока не будет произведена сборка мусора. В принципе, это своеобразная страховка, на случай, если что-то пойдет не так. Если у вас сохранились ссылки на старые commit-ы, то они будут сохранены, пока ссылки существуют.
НЕ следует выполнять rebase для веток, поверх которых другие люди создали commit-ы. Восстановить их можно (и даже не очень сложно), однако это привносит путаницу и массу бесполезной работы.
Вот как все выглядит после сборки мусора (либо игнорирования недоступных commit-ов), и создания нового commit-а поверх ветки, к которой применили rebase.
Также, с помощью rebase можно перемещать одновременно несколько commit-ов.
На этом все. Надеюсь, материал будет полезен.