Контроль версий (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; данные будут храняться в директории.git
git 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 merge
git 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.