Контроль версий (Git)
Системы контроля версий (VCS) - это инструменты, используемые для отслеживания изменений исходного кода (или других коллекций файлов и папок). Как следует из названия, эти инструменты помогают сохранять историю изменений; кроме того, они облегчают совместную работу. VCS отслеживают изменения в папке и ее содержимом в серии снимков, где каждый снимок охватывает всё состояние файлов/папок внутри верхнего уровня каталога. VCS также сохраняют метаданные, такие как кто создал каждый снимок, сообщения, ассоциированные с каждым снимком, и так далее.
Почему система контроля версий полезна? Даже когда вы работаете самостоятельно, она позволяет вам просматривать старые снимки проекта, вести лог причин внесения определенных изменений, работать на параллельных ветках разработки и многое другое. При работе с другими людьми это незаменимый инструмент для просмотра изменений, внесенных другими людьми, а также для разрешения конфликтов при параллельной разработке.
Современные системы контроля версий также позволяют легко (и часто автоматически) отвечать на вопросы вроде:
- Кто написал этот модуль?
- Когда была отредактирована эта конкретная строка этого конкретного файла? Кем? Почему она была отредактирована?
- За последние 1000 ревизий, когда/почему перестал работать определенный модульный тест?
Хотя существуют и другие системы контроля версий, Git является фактическим стандартом для контроля версий. Этот комикс XKCD отражает репутацию Git:

Поскольку интерфейс 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
git help <command>: получить подсказку по команде gitgit init: создает новый репозиторий git; данные будут храняться в директории.gitgit status: сообщает, что происходитgit add <filename>: добавляет файлы в промежуточную областьgit commit: создает новый коммит- Пишите хорошие сообщения коммитов!
- Еще больше причин писать хорошие сообщения коммитов!
git log: показывает историю измененийgit log --all --graph --decorate: визуализирует историю в виде DAGgit diff <filename>: показывает изменения, которые вы сделали относительно промежуточной областиgit diff <revision> <filename>: показывает различия в файле между снимкамиgit checkout <revision>: обновляет HEAD и текущую ветку
Ветвление и слияние (Branching and merging)
git branch: показывает веткиgit branch <name>: создает веткуgit checkout -b <name>: создает ветку и переключается на нее- то же самое, что
git branch <name>; git checkout <name>
- то же самое, что
git merge <revision>: выполняет слияние в текущую веткуgit mergetool: использует специальный инструмент для помощи в разрешении конфликтов при слиянииgit rebase: последовательно применяет набор коммитов поверх определенного коммита
Удаленные репозитории
git remote: список удаленных репозиториевgit remote add <name> <url>: добавить удаленный репозиторийgit push <remote> <local branch>:<remote branch>: отправить объекты в удаленный репозиторий и обновить удаленную ссылкуgit branch --set-upstream-to=<remote>/<remote branch>: установить соответствие между локальной и удаленной веткойgit fetch: получить объекты/ссылки из удаленного репозиторияgit pull: то же самое, чтоgit fetch; git mergegit clone: скачать репозиторий с удаленного сервера
Отмена
git commit --amend: редактирование содержимого/сообщения коммитаgit reset HEAD <file>: отмена добавления файлаgit checkout -- <file>: отклонение изменений
Продвинутый Git
git config: Git высоконастраиваемgit clone --depth=1: поверхностное клонирование, без полной истории версийgit add -p: интерактивное добавлениеgit rebase -i: интерактивное перебазированиеgit blame: показать, кто последний редактировал каждую строкуgit stash: временное удаление изменений рабочего каталогаgit bisect: бинарный поиск по истории (например, для регрессий).gitignore: указать намеренно неотслеживаемые файлы для игнорирования
Разное
- Графические интерфейсы: существует множество графических клиентов для Git. Мы лично их не используем и вместо этого используем интерфейс командной строки.
- Интеграция с оболочкой: очень удобно видеть статус Git внутри вашей оболочки (zsh, bash). Часто включено в фреймворки, такие как Oh My Zsh.
- Интеграция с редактором: аналогично вышеуказанному, удобные интеграции со многими функциями. fugitive.vim является стандартным для Vim.
- Рабочие процессы: мы научили вас модели данных, плюс некоторые базовые команды; мы не говорили вам, какие практики следует соблюдать при работе над большими проектами (существует много различных подходов).
- GitHub: Git не является GitHub. У GitHub есть специфический способ внесения кода в другие проекты, называемый pull requests.
- Другие провайдеры Git: GitHub не единственный: существует множество хостов репозиториев Git, таких как GitLab и BitBucket.
Ресурсы
- Pro Git - настоятельно рекомендуется к прочтению. Изучение глав 1-5 научит вас большей части того, что вам нужно знать для эффективного использования Git, теперь, когда вы понимаете модель данных. Последующие главы содержат интересный и продвинутый материал.
- Oh Shit, Git!?! - короткое руководство о том, как исправлять распространенные ошибки в Git.
- Git for Computer Scientists - это короткое объяснение модели данных Git, с меньшим количеством псевдокода и большим количеством сложных диаграмм.
- Git from the Bottom Up - подробное объяснение деталей реализации Git; для любопытных.
- How to explain git in simple words
- Изучите Git Branching - это браузерная игра, которая учит вас Git.
Упражнения
- Если у вас нет предыдущего опыта работы с Git, попробуйте прочитать первые несколько глав Pro Git или пройдите туториал, например, Learn Git Branching.
- Склонируйте репозиторий веб-сайта класса.
- Исследуйте историю версий, визуализируя ее в виде графа.
- Кто последним изменил
README.md? (Подсказка: используйтеgit log) - Какое сообщение коммита было связано с последней модификацией
строки
collections:в_config.yml? (Подсказка: используйтеgit blameиgit show)
- Одна из распространенных ошибок при изучении Git - это коммит больших файлов, которые не должны управляться через Git; или добавление конфиденциальной информации. Попробуйте добавить файл в репозиторий, сделать несколько коммитов, а затем удалить этот файл из истории (возможно, вы захотите посмотреть это).
- Склонируйте какой-нибудь репозиторий с GitHub и измените один из его существующих файлов.
Что происходит, когда вы делаете
git stash? Что вы видите при выполненииgit log --all --oneline? Выполнитеgit stash pop, чтобы отменить то, что вы сделали сgit stash. В каком сценарии это может быть полезно? - Как и многие инструменты командной строки, Git предоставляет файл конфигурации (или dotfile)
под названием
~/.gitconfig. Создайте псевдоним в~/.gitconfig, чтобы при выполненииgit graphвы получали выводgit log --all --graph --decorate --oneline. - Вы можете определить глобальные шаблоны игнорирования в
~/.gitignore_globalпосле выполненияgit config --global core.excludesfile ~/.gitignore_global. Сделайте это и настройте ваш глобальный файл gitignore так, чтобы он игнорировал временные файлы, специфичные для ОС или редактора, например,.DS_Store. - Создайте форк репозитория веб-сайта класса, найдите опечатку или какое-то другое улучшение, которое вы можете сделать, и отправьте пулл-реквест на GitHub.
Лицензия CC BY-NC-SA.