Контроль версий (Git)

Системы контроля версий (VCS) - это инструменты, используемые для отслеживания изменений исходного кода (или других коллекций файлов и папок). Как следует из названия, эти инструменты помогают сохранять историю изменений; кроме того, они облегчают совместную работу. VCS отслеживают изменения в папке и ее содержимом в серии снимков, где каждый снимок охватывает всё состояние файлов/папок внутри верхнего уровня каталога. VCS также сохраняют метаданные, такие как кто создал каждый снимок, сообщения, ассоциированные с каждым снимком, и так далее.

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

Современные системы контроля версий также позволяют легко (и часто автоматически) отвечать на вопросы вроде:

Хотя существуют и другие системы контроля версий, Git является фактическим стандартом для контроля версий. Этот комикс XKCD отражает репутацию Git:

xkcd 1597

Поскольку интерфейс Git является “протекающей” абстракцией, изучение Git “сверху вниз” (начиная с его интерфейса / командной строки) может привести к большому количеству путаницы. Вполне возможно запомнить несколько команд и воспринимать их как магические заклинания, и следовать подходу в приведенном выше комиксе, когда что-то идет не так.

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

модель данных Git

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

Snapshots

Git моделирует историю коллекции файлов и папок внутри некоторого каталога верхнего уровня как серию снимков (snapshots). В терминологии Git, файл называется “blob”, и это просто набор байтов. Каталог называется “деревом”, и он отображает имена на blob’ы или деревья (так что каталоги могут содержать другие каталоги). Снимок - это дерево верхнего уровня, которое отслеживается. Например, у нас может быть следующее дерево:

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

Дерево верхнего уровня содержит два элемента: дерево “foo” (которое само содержит один элемент, blob “bar.txt”) и blob “baz.txt”.

Модель истории: связанные снимки

Как система контроля версий должна соотносить снимки? Простая модель могла бы иметь линейную историю. История была бы списком снимков в хронологическом порядке. По многим причинам, Git не использует такую модель.

В Git история представляет собой направленный ациклический граф (DAG) снимков. Это может звучать как сложный математический термин, но не стоит пугаться. Все, что это значит, это то, что каждый снимок в Git ссылается на набор “родителей”, предшествующих снимков. Это набор родителей, а не один родитель (как было бы в случае линейной истории), потому что снимок может происходить от нескольких родителей, например, из-за объединения (merging) двух параллельных ветвей разработки.

Git называет эти снимки “commit”s. Визуализация истории коммитов может выглядеть примерно так:

o <-- o <-- o <-- o
            ^
             \
              --- o <-- o

На ASCII-арт выше, o соответствуют отдельным коммитам (снимкам). Стрелки указывают на родителя каждого коммита (это отношение “предшествует”, а не “следует за”). После третьего коммита история разветвляется на две отдельные ветви. Это может соответствовать, например, двум отдельным фичам, разрабатываемым параллельно, независимо друг от друга. В будущем эти ветви могут быть объединены для создания нового снимка, который включает обе фичи, создавая новую историю, которая выглядит примерно так, с новым объединенным коммитом, показанным жирным шрифтом:


o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Коммиты в Git являются неизменяемыми. Это, однако, не означает, что ошибки не могут быть исправлены; просто “редактирование” истории коммитов на самом деле создает совершенно новые коммиты, и ссылки (см. ниже) обновляются, чтобы указывать на новые.

Модель данных, псевдокод

Может быть полезно увидеть модель данных Git, записанную в псевдокоде:

// a file is a bunch of bytes
type blob = array<byte>

// a directory contains named files and directories
type tree = map<string, tree | blob>

// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

Это простая модель истории.

Объекты и адресация по содержимому (Objects and content-addressing)

“Объект” - это blob, дерево, или коммит:

type object = blob | tree | commit

В хранилище данных Git все объекты адресуются по их хешу SHA-1.

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

Blob’ы, деревья и коммиты объединены следующим образом: они все являются объектами. Когда они ссылаются на другие объекты, они на самом деле не содержат их в своем представлении на диске, но имеют ссылку на них по их хешу.

Например, дерево из примера сверху (визуализация с помощью git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d), выглядит следующим образом:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

Дерево содержит указатели до его содержимого, baz.txt (blob) и foo (дерево). Если мы посмотрим на содержание по адресу хэша, соответствующего файлу baz.txt с помощью git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85, мы получим:

git is wonderful

Ссылки

Все снимки можно идентифицировать по их хешу SHA-1. Это неудобно, потому что людям сложно запоминать строки из 40 шестнадцатеричных символов.

Решение Git для этой проблемы - это понятные человеку имена для хешей SHA-1, называемые “ссылками”. Ссылки - это указатели на коммиты. В отличие от объектов, которые являются неизменяемыми, ссылки являются изменяемыми (могут быть обновлены для указания на новый коммит). Например, ссылка master обычно указывает на последний коммит в основной ветке.

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

С их помощью Git может использовать понятные людям имена, такие как “master”, чтобы ссылаться на определенный снимок в истории, вместо длинной шестнадцатеричной строки.

Важная деталь заключается в том, что нам часто нужно понятие “где мы сейчас находимся” в истории, чтобы когда мы делаем новый снимок, мы знали, относительно чего он сделан (как мы устанавливаем поле parents коммита). В Git это “где мы сейчас находимся” - это специальная ссылка под названием “HEAD”.

Репозитории

Наконец, мы можем определить, что (примерно) такое Git репозиторий: это данные объекты и ссылки.

На диске все, что хранит Git, это объекты и ссылки: это все, что есть в модели данных Git. Все команды git соответствуют некоторой манипуляции с DAG коммитов путем добавления объектов и добавления/обновления ссылок.

Каждый раз, когда вы вводите команду, подумайте о том, какие манипуляции она делает со структурой данных графа. Если вы пытаетесь сделать определенный вид изменения в DAG коммитов, например, “отменить неподтвержденные изменения и сделать ссылку ‘master’ указывающей на коммит 5d83f9e”, вероятно, есть команда для этого (например, в этом случае, git checkout master; git reset --hard 5d83f9e).

Промежуточная область

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

Один из способов, которым вы могли бы представить реализацию создания снимков - это иметь команду “создать снимок”, которая создает новый снимок на основе текущего состояния рабочего каталога. Некоторые инструменты контроля версий работают так, но не Git. Мы хотим чистые снимки, и не всегда идеально делать снимок из текущего состояния. Например, представьте себе сценарий, когда вы реализовали две отдельные функции, и вы хотите создать два отдельных коммита, где первый вводит первую функцию, а следующий вводит вторую. Или представьте сценарий, когда у вас есть отладочные операторы print, добавленные по всему вашему коду, вместе с исправлением ошибки; вы хотите закомитить исправление ошибки, а не операторы print.

Git учитывает такие сценарии, позволяя вам указывать, какие изменения должны быть включены в следующий снимок через механизм, называемый “промежуточной областью” (staging area).

Интерфейс командной строки Git

Чтобы избежать дублирования информации, мы не собираемся подробно объяснять команды. Для получения дополнительной информации рекомендуется ознакомиться с Pro Git или посмотреть видео лекции.

Basics

Ветвление и слияние (Branching and merging)

Удаленные репозитории

Отмена

Продвинутый Git

Разное

Ресурсы

Упражнения

  1. Если у вас нет предыдущего опыта работы с Git, попробуйте прочитать первые несколько глав Pro Git или пройдите туториал, например, Learn Git Branching.
  2. Склонируйте репозиторий веб-сайта класса.
    1. Исследуйте историю версий, визуализируя ее в виде графа.
    2. Кто последним изменил README.md? (Подсказка: используйте git log)
    3. Какое сообщение коммита было связано с последней модификацией строки collections: в _config.yml? (Подсказка: используйте git blame и git show)
  3. Одна из распространенных ошибок при изучении Git - это коммит больших файлов, которые не должны управляться через Git; или добавление конфиденциальной информации. Попробуйте добавить файл в репозиторий, сделать несколько коммитов, а затем удалить этот файл из истории (возможно, вы захотите посмотреть это).
  4. Склонируйте какой-нибудь репозиторий с GitHub и измените один из его существующих файлов. Что происходит, когда вы делаете git stash? Что вы видите при выполнении git log --all --oneline? Выполните git stash pop, чтобы отменить то, что вы сделали с git stash. В каком сценарии это может быть полезно?
  5. Как и многие инструменты командной строки, Git предоставляет файл конфигурации (или dotfile) под названием ~/.gitconfig. Создайте псевдоним в ~/.gitconfig, чтобы при выполнении git graph вы получали вывод git log --all --graph --decorate --oneline.
  6. Вы можете определить глобальные шаблоны игнорирования в ~/.gitignore_global после выполнения git config --global core.excludesfile ~/.gitignore_global. Сделайте это и настройте ваш глобальный файл gitignore так, чтобы он игнорировал временные файлы, специфичные для ОС или редактора, например, .DS_Store.
  7. Создайте форк репозитория веб-сайта класса, найдите опечатку или какое-то другое улучшение, которое вы можете сделать, и отправьте пулл-реквест на GitHub.

Редактировать страницу.

Лицензия CC BY-NC-SA.