Git — это простая, но очень мощная система. Большинство людей пытаются обучить других пользоваться Git демонстрируя пару дюжин команд, а затем восклицая «та-да!». Я считаю, что этот способ несовершенен. После такого можно научиться использовать Git для выполнения простых задач, но команды Git будут всё равно казаться волшебными заклинаниями. Попытки сделать что-нибудь необычное будут ужасными. До тех пор, пока не возникнет понимания концепций, на которых построен Git, ты будешь чувствовать себя чужаком в стране чужой. Эта басня расскажет о создании системы, похожей на Git, с нуля. Понимание концепций, представленных здесь, будет самым ценным, что ты можешь сделать для подготовки к освоению всей мощи Git. Сами концепции весьма просты, но обеспечивают потрясающее богатство функциональности. Прочитай эту басню до конца, и у тебя не должно остаться проблем с освоением различных команд Git и взятием под контроль той огромной мощи, которую он тебе предоставляет.

Басня

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

Снимки

Альфред — твой друг, который работает в торговом центре в одной из кабинок «Особые моменты». Весь день он фотографирует маленьких детей, неуклюже позирующих на фоне джунглей или океана. Во время одного из ваших постоянных обедов возле киоска с кренделями, Альфред рассказывает тебе историю о женщине по имени Хейзел, которая приводит свою дочь и просит сделать её портрет каждый год в один и тот же день. «Она приносит все её предыдущие фотографии, — говорит тебе Альфред. — Она любит вспоминать, как выглядела её дочь на каждом отдельном этапе, как будто снимки действительно позволяют ей путешествовать назад и вперёд во времени к своим воспоминаниям». Как обычно происходит в детективах, невинное замечание Альфреда срабатывает как катализатор, и ты осознаёшь идеальное решение твоей проблемы контроля версий. Снимки (snapshots), как точки сохранения в видеоигре, действительно то, что тебе нужно при работе с системой контроля версий. Что, если бы ты мог брать снимки твоего кода в любой момент и восстанавливать их по требованию? Альфред читает выражение осознания, заполнившее твоё лицо, и понимает, что ты сейчас молча уйдёшь домой реализовывать что бы гениального ты там ни придумал благодаря нему. И ты его не разочаровываешь. Ты начинаешь свой проект в каталоге с именем working. По мере программирования, ты решаешь писать за раз по одной функции. Когда ты заканчиваешь самостоятельную часть функции, ты сохраняешь все файлы и делаешь копию всего рабочего каталога, называя его snapshot-0. После этой операции копирования, ты больше никогда не будешь изменять файлы в этом новом каталоге. После следующего этапа работы, ты снова копируешь, только теперь каталог уже называется snapshot-1, и так далее. Чтобы было проще вспомнить, какие изменения ты сделал в каждом снимке, ты добавляешь специальный файл по имени message к каждому снимку, который содержит описание работы, которую ты сделал, и дату завершения. Выводя на печать содержимое каждого сообщения, легко найти любое изменение, которое ты сделал в прошлом, в случае необходимости воскресить старый код.

Ветки

Через некоторое время, начинает проявляться что-то похожее на релиз. Долгие ночи за клавиатурой наконец-то выдают snapshot-99, который и станет Релизом Версии 1.0. Без лишней волокиты, снимок упаковывается и раздаётся нетерпеливо ждущим массам. Воодушевлённый прекрасным приёмом твоей программы публикой, ты рвёшься вперёд, стремясь сделать следующую версию ещё более успешной. Твоя система контроля версий до сих пор была верным помощником. Старые версии кода никуда не исчезают, и легко доступны. Но вскорости после релиза, начинают приходить баг-репорты. Никто не идеален, говоришь ты себе, и snapshot-99 готов к восстановлению для исправления ошибок. После релиза, ты создал 10 новых снимков. Эта новая работа не должна быть включена в версию 1.0.1, в которой ты собираешься только исправить ошибки. Чтобы решить проблему, ты копируешь snapshot-99 в working, чтобы твой рабочий каталог совпадал с тем, что было в Версии 1.0. Немного строк кода и баг в рабочем каталоге исправлен. Вот здесь-то проблема и проявляет себя. Система контроля версий хорошо работает при линейной разработке, но впервые тебе понадобилось создать снимок, который не является прямым потомком предыдущего. Если ты создашь snapshot-110 (вспомни, что ты уже сделал 10 снимков после релиза), то ты прервёшь линейный поток и ни для одного снимка не будет известно, что было до него. Очевидно, что тебе нужно нечто более мощное, чем линейная система. Исследования показали, что даже короткое пребывание на природе может помочь восстановить творческий потенциал мозга. Ты дни напролёт сидел и смотрел на искусственно поляризованный свет твоего монитора. Прогулка по лесу и свежий осенний воздух помогут тебе, и если повезёт, ты даже придумаешь как решить проблему. Огромные дубы, вдоль которых ты идёшь, всегда тебе нравились. Они величественно и гордо стоят на фоне идеально голубого неба. Половина покрасневших листьев покинула дерево, обнажая сложный узор веток. Зацепившись взглядом за один из тысяч кончиков веток, ты неторопливо пытаешься отследить её до их единого ствола. Эта органически созданная структура может быть сколь угодно сложной, но правила нахождения обратного пути к стволу так просты и идеально подходят для отслеживания множества линий разработки! Оказывается, правду говорят про природу и творчество. Если рассматривать историю кода как дерево, решение проблемы поиска предков становится тривиальным. Надо только добавить название снимка-отца в файл message, который ты пишешь для каждого снимка. Добавив только один указатель, можно легко и точно отследить историю любого снимка до самого корня.

Имена веток

История твоего кода теперь представляет из себя дерево. Вместо единственного последнего снимка, у тебя теперь два: по одному на каждую ветку. При линейной системе, последовательное нумерование позволяло легко идентифицировать последний снимок. Теперь этой возможности нет. Создавать новые ветки разработки стало так просто, что ты захочешь пользоваться этим всё время. Ты будешь создавать ветки для исправлений старых релизов, для экспериментов, которые могут быть неудачными; на самом деле, вполне можно создавать новую ветку для каждой функции, которую ты начинаешь разрабатывать! Но за всё хорошее надо платить. Каждый раз создавая снимок, ты должен помнить, что он становится последним в своей ветке. Без этой информации переключение на новую ветку станет весьма трудоёмким процессом. Каждый раз создавая новую ветку, ты возможно мысленно назовёшь её как-нибудь. «Это будет Ветка Доработки Версии 1.0,» — возможно скажешь ты. Может быть ты даже станешь называть ветку, в которой раньше велась линейная разработка, основной («master»). Но подумай об этом ещё. В контексте дерева, что означает назвать ветку? Можно назвать каждый снимок в истории ветки её именем, но это потенциально потребует хранения большого объёма данных. И это всё ещё никак не способствует эффективному поиску последнего снимка ветки. Минимальные сведения для идентификации ветки — местонахождение последнего её снимка. Если тебе надо узнать список снимков, входящих в ветку, отследить предков последнего снимка не составит труда. Хранение названий веток тривиально. В файле с именем branches, хранящемся снаружи всех снимков, просто перечисли пары имя/снимок, которые представляют кончики веток. Чтобы переключиться на ветку с данным именем, тебе потребуется всего лишь посмотреть номер соответствующего ей снимка в этом файле. Поскольку ты хранишь только номер последнего снимка ветки, создание снимка теперь включает дополнительный шаг. Если создаваемый снимок является частью ветки, то нужно обновить файл branches, чтобы имя ветки стало ассоциироваться с новым снимком. Не такая уж и высокая цена за получаемые преимущества.

Теги

Попользовавшись ветками некоторое время, ты замечаешь, что они могут служить двум целям. Во-первых, они могут являться передвигающимися указателями на снимки для отслеживания кончиков веток. Во-вторых, они могут указывать на один снимок и никогда не двигаться. Первый вариант позволяет отслеживать процесс разработки, такие вещи как «Доработка Релиза». Второй можно использовать для пометки интересных точек истории, например «Версия 1.0» и «Версия 1.0.1». Смешивание веток обоих типов в одном файле попахивает бардаком. Оба типа веток — указатели на снимки, но одни двигаются, а другие нет. Ради ясности и элегантности, ты решаешь создать другой файл с именем tags, в который и помещаешь указатели второго типа. Хранение разных типов указателей в отдельных файлах снизит вероятность того, что ты случайно воспользуешься тегом как веткой или наоборот.

Распределённость

Работать в одиночку становится скучно. Разве не лучше было бы пригласить друга работать над твоим проектом? Так вот, тебе повезло. У твоей подруги Зои есть компьютер вроде твоего и она хочет помочь тебе с проектом. Поскольку ты создал такую шикарную систему контроля версий, ты незамедлительно рассказываешь всё о ней Зое и отправляешь ей копию всех своих снимков, веток и тегов, чтобы у неё были все возможности доступа к истории кода. Тебе нравится, что Зоя в твоей команде, но у неё есть привычка надолго уезжать в дальние края без доступа к интернету. Как только у неё появился исходный код, она села на рейс до Патагонии и пропала без вести на неделю. В это время вы оба усиленно программируете. Когда она наконец возвращается, вы обнаруживаете критический недостаток в вашей системе контроля версий. Из-за того, что вы оба использовали одну и ту же систему нумерования, теперь у вас обоих каталоги с именами snapshot-114, snapshot-115, и так далее, но с разным содержимым! Хуже того, вы даже не знаете кто в какие снимки вносил изменения. Вместе, вы придумали план решения этих проблем. Во-первых, отныне сообщения в снимках будут содержать имя автора и адрес электронной почты. Во-вторых, снимки больше не будут называться просто числами. Вместо этого, вы будете хешировать содержимое сообщения. Этот хеш будет гарантированно уникальным, так как никакие два сообщения не могут содержать одну и ту же дату, текст, снимок-родитель и имя автора. Чтобы всё точно работало как надо, вы оба согласились использовать алгоритм хеширования SHA1, который берёт содержимое файла и возвращает 40-символьную шестнадцатеричную строку. Вы оба обновляете истории по новой технике, и вместо конфликтующих каталогов snapshot-114, у вас теперь различные каталоги с именами 8ba3441b6b89cad23387ee875f2ae55069291f4b и db9ecb5b5a6294a8733503ab57577db96ff2249e. С новой схемой именования, тривиальной задачей становится скачать все новые снимки с компьютера Зои и разместить их вместе с твоими существующими снимками. Поскольку каждый снимок указывает на родителя, а идентичные сообщения (а следовательно и одинаковые снимки) имеют идентичные имена несмотря на то, где они были созданы, история кода всё ещё представляет из себя дерево. Только теперь это дерево состоит и из твоих снимков и из снимков Зои. Этот факт достаточно важен, чтобы его повторить. Снимок идентифицируется хешем SHA1, который уникально его (и его предка) определяет. Эти снимки могут быть созданы и перемещены между компьютерами без потери их индивидуальности и места, где они находятся в дереве истории проекта. Более того, снимками можно делиться, а можно хранить у себя, по желанию. Если у тебя есть какие-то экспериментальные снимки, которые ты не хочешь никому показывать, это легко устроить. Просто не давай их Зое!

Автономность

Привычка Зои постоянно путешествовать приводит к тому, что она проводит несчётные часы в самолётах и кораблях. Большинство мест, которые она посещает, не оборудованы доступом в интернет. В итоге она проводит больше времени оффлайн чем онлайн. Неудивительно, что Зое так нравится твоя система контроля версий. Все повседневные операции, которые ей нужны, могут быть осуществлены локально. Сетевое соединение ей нужно только тогда, когда она готова поделиться своими снимками с тобой.

Слияние

Перед тем как Зоя отправилась в своё путешествие, ты попросил её начать работать с веткой math и реализовать функцию, генерирующую простые числа. Тем временем, ты тоже разрабатывал в ветке math, только ты писал функцию, генерирующую волшебные числа. После возвращения Зои, перед вами встала задача слияния этих двух отдельных веток разработки в один снимок. Так как вы оба работали над отдельными задачами, слияние осуществляется легко. При написании сообщения для снимка слияния, ты осознал, что этот снимок — особенный. Вместо одного предка, у этого снимка слияния их аж два! Первый предок — это твой последний снимок в ветке math, а второй — последний снимок Зои в её ветке math. Сам снимок слияния не содержит никаких изменений, кроме тех, что нужны для объединения двух различных снимков в один программный код. После того, как ты закончил слияние, Зоя скачивает все твои снимки, которых нет у неё, что включает и твою разработку в ветке math и твой снимок слияния. После этого, ваши истории становятся совершенно одинаковыми!

Переписывание истории

Как и многие разработчики программного обеспечение, ты стремишься поддерживать чистоту и очень хорошую организацию кода. Это также подразумевает поддержку опрятной истории разработки. Вчера вечером ты пришёл домой, слегка переборщив Гинессом в местном пабе, и начал программировать, создав по пути несколько снимков. Сегодня утром, просматривая код, тебя слегка затрясло. В целом он хороший, но в самом начале ты допустил много ошибок, которые исправил в последующих снимках. Допустим, ты вёл свою пьяную разработку в ветке drunk и сделал три снимка с тех пор, как вернулся из бара. Если название drunk указывает на последний снимок в этой ветке, то ты можешь воспользоваться удобным обозначением предка этого снимка. Запись drunk^ означает предка снимка, на который указывает имя ветки drunk. Аналогично, drunk^^ означает дедушку снимка drunk. Таким образом, три снимка в хронологическом порядке выглядят так: drunk^^, drunk^ и drunk. На самом деле, ты бы хотел видеть вместо этих трёх плохих снимков два хороших. В одном изменяется существующая функция, а в другом добавляется новый файл. Чтобы добиться этого, ты копируешь drunk в working и удаляешь файл, который был создан в последнем снимке. Теперь в working находится правильные изменения в существующей функции. Ты делаешь новый снимок каталога working и пишешь соответствующее сообщение. В качестве предка ты указываешь хеш SHA1 снимка drunk^^^, по сути создав новую ветку с того же снимка, что и вчера вечером. Теперь ты копируешь drunk в working и создаёшь снимок с новым файлом. В качестве предка ты указываешь снимок, созданный на предыдущем шаге. В последнюю очередь, ты изменяешь указатель ветки drunk на последний созданный снимок. История ветки drunk теперь представляет улучшенную версию того, что ты сделал вчера. Те три снимка, которые ты заменил, больше не нужны, так что ты можешь удалить их или оставить в назидание потомкам. На них не указывает ни одно имя ветки, поэтому их потом будет трудно найти, но если ты их не удалишь, они будут просто там торчать.

Временная область

Как бы ты ни пытался делать модификации, относящиеся только к одной функции или логическому участку, иногда тебя заносит и ты начинаешь делать что-то совершенно не относящееся к текущей работе. Только наполовину закончив, ты осознаёшь, что твой рабочий каталог теперь содержит то, что на самом деле должно быть двумя отдельными снимками. Помочь тебе в этой раздражающей ситуации призвана концепция временной области (staging area). Она служит промежуточным шагом между твоим рабочим каталогом и окончательным снимком. Каждый раз, когда ты заканчиваешь снимок, ты копируешь его в каталог staging. Теперь, как только ты заканчиваешь редактировать файл, создаёшь новый файл или удаляешь старый в рабочем каталоге, ты можешь решить, должно ли это изменение войти в следующий снимок. Если должно, ты делаешь такое же изменение в папке staging. Если нет, ты можешь оставить его в working и сделать частью следующего снимка. С этих пор, снимки создаются прямо с каталога staging. Это разделение процессов программирования и подготовки временной области упрощает разделение того, что должно, и того, что не должно войти в следующий снимок. Тебе больше не надо сильно волноваться по поводу случайного, не связанного с текущей работой, изменения в рабочем каталоге. Однако надо соблюдать некоторую осторожность. Возьмём, к примеру, файл README. Ты вносишь изменение в этот файл, а затем такое же в staging. Затем продолжаешь делать своё дело, редактировать другие файлы. Через некоторое время ты вносишь ещё одно изменение в README. Теперь у тебя в нём два изменения, но только одно из них отражено во временной области! Если ты создашь снимок сейчас, второго изменения там не будет. Мораль такова: каждое новое изменение должно быть добавлено во временную область, если оно должно стать частью следующего снимка.

Диффы

Имея рабочий каталог, временную область и кучу снимков, становится сложно понять, какая конкретно разница в коде между этими каталогами. Сообщение в снимке даёт только общее описание изменений, а не конкретные изменённые строки файлов. Используя алгоритм сравнения файлов, ты можешь создать небольшую программку, которая будет показывать разницу между двумя каталогами с кодом. По мере разработки и копирования изменений из рабочего каталога во временную область, ты захочешь узнать, чем они отличаются, чтобы определить, что ещё необходимо поместить во временную область. Также важно знать, в чём разница между временной областью и последним снимком, поскольку именно эти изменения и составят следующий снимок. Есть множество других диффов (diff, результат сравнения текстовых файлов), которые могут быть интересны. Разница между конкретным снимком и его предком покажет набор изменений (changeset), который привнёс этот снимок. Дифф между двумя ветками поможет выяснить, как далеко разошлись две ветки разработки.

Исключение избыточности

После ещё нескольких путешествий по Намибии, Стамбулу и Галапагосу, Зоя начала жаловаться, что её жёсткий диск заполняется сотнями практически одинаковых копий программы. У тебя тоже уже возникло ощущение избыточности дублирующихся файлов. После недолгих раздумий, ты придумал кое-что очень умное. Ты помнишь, что хеш SHA1 выдаёт короткую строку, уникальную для заданного содержимого файла. С самого первого снимка в истории проекта ты начинаешь процедуру преобразования. Для начала, ты создаёшь каталог с именем objects снаружи каталогов с кодом. Далее, ты находишь наиболее глубоко вложенный каталог в снимке. Также, ты открываешь временный файл на запись. Для каждого файла в этом каталоге ты выполняешь три действия. Первое: вычисляешь хеш SHA1 его содержимого. Второе: добавляешь запись во временный файл со словом ‘blob’ (binary large object, большой двоичный объект), хешем SHA1 из первого действия и именем файла. Третье: копируешь файл в каталог objects и переименовываешь его в тот же хеш. Как только ты заканчиваешь со всеми файлами, вычисляешь SHA1 временного файла, сохраняешь его также в каталоге objects с именем, совпадающим с хешем. Если при записи файла в каталог objects, там уже содержится файл с тем же именем, то это значит, что ты уже сохранил содержимое этого файла и нет нужды это делать снова. Теперь перейди в каталог выше и начни заново. Только на этот раз, когда дойдёшь до каталога, который только что обработал, пиши слово ‘tree’, хеш SHA1 временного файла от предыдущего каталога и имя каталога в новый временный файл. Таким образом ты можешь построить дерево файлов-объектов, соответствующих каталогам. Эти файлы будут содержать хеши SHA1 и имена файлов и вложенных каталогов, которые находятся в соответствующих каталогах. Как только эта процедура будет завершена для каждого каталога и файла в снимке, у тебя будет единственный файл-объект для корневого каталога и соответствующий ему SHA1. Поскольку ничто не содержит корневой каталог, ты должен записать его хеш куда-нибудь. Идеальным местом для этого будет файл с сообщением из снимка. Таким образом, уникальность хеша SHA1 сообщения также будет зависеть от всего содержимого снимка, и ты можешь гарантировать с абсолютной уверенностью, что снимки с одинаковыми хешами сообщений содержат одни и те же файлы! Также удобно создать объект из сообщения снимка, как ты делал для блобов и деревьев. Поскольку ты поддерживаешь список имён веток и тегов, которые указывают на хеши сообщений, тебе не придётся беспокоиться о потере снимков, которые важны для тебя. Со всей этой информацией в каталоге objects, ты можешь без проблем удалить каталог снимка, который ты преобразовал. Если тебе понадобится восстановить снимок, тебе просто придётся взять SHA1 корневого дерева, записанный в сообщении, и восстановить из каждого дерева и блоба соответствующие подкаталоги и файлы. Для одного снимка это преобразование ничего особенного тебе не дало. Ты просто превратил одну файловую систему в другую и создал себе дополнительные трудности. Но настоящая выгода от этой системы проявляется при повторном использовании деревьев и блобов от разных снимков. Представь себе два последовательных снимка, которые отличаются лишь одним файлом в корневом каталоге. Если оба снимка содержат 10 каталогов и 100 файлов, процесс преобразования создаст 10 деревьев и 100 блобов для первого снимка, но только одно дерево и один блоб для второго!

Сжатие блобов

Избавившись от дублирования блобов и деревьев, ты существенно снизил общий размер истории твоего проекта. Но это не всё, что ты можешь сделать для экономии места. Исходный код — это просто текст. А текст можно очень эффективно сжимать, используя алгоритм сжатия вроде LZW и DEFLATE. Если ты сожмёшь каждый блоб перед вычислением его SHA1 и сохранением на диск, снижение размера истории твоего проекта снова окажется впечатляющим.

Настоящий Git

Система контроля версий, которую ты построил, теперь в достаточной степени похожа на Git. Основное отличие в том, что Git предоставляет очень хорошие утилиты командной строки для осуществления таких вещей как создание новых снимков и переключение на старые (Git использует термин «коммит» (commit) вместо «снимок»), отслеживание истории, поддержка кончиков веток, загрузка новых изменений у других людей, сливание и сравнение веток, и сотни других распространённых (и не очень) задач. Продолжая изучать Git, помни эту басню. Git действительно очень простой по сути, и именно эта простота делает его таким гибким и мощным. Ещё одна вещь напоследок, пока ты не побежал изучать все команды Git: помни, что практически невозможно потерять работу, которая была закоммичена. Даже когда ты удаляешь ветку, на самом деле исчезает только указатель на коммит. Все снимки всё ещё хранятся в каталоге объектов, достаточно только найти хеш SHA1 коммита. В таких случаях, попробуй git reflog. Он содержит историю того, на что указывала каждая ветка, и в трудную минуту, он тебя выручит. Вот некоторые места, где можно продолжить изучение. А теперь беги и становись мастером Git!
Creative Commons License