2009-09-08

SCM // 4. Контроль версий

И снова здравствуйте.

Продолжаю публиковать цикл статей о SCM — управлении конфигурацией ПО.
3 предыдущие заметки можно прочитать в этом же блоге.

Сегодня расскажу о том, с чем работает большинство читателей — о контроле версий.


Disclaimer

Далее будут описаны основные техники, реализованные в подавляющем большинстве систем контроля версий. Как они реализуются в приложениях, которые использует читатель, оставим на откуп многочисленным руководствам пользователя, how-to, FAQ и прочим документам, коих можно найти без труда. Главное – понять, по каким принципам и зачем оно работает именно так.

О чем речь?

Система контроля версий – это программное обеспечение, позволяющее создавать версии элементов и работать с этими версиями, как с самостоятельными элементами. В англоязычных источниках используется термин version control systems, сокращенно VCS. Работа с версиями предполагает как создание самих версий, так и структуры для их хранения. Как правило, это или цепочки, или деревья.

Прежде чем работать с элементами и их версиями, надо эти элементы создать, т.е. дать указать системе контроля версий взять имеющиеся объекты реального мира и поместить их под свой контроль. Вместе с самим элементом всегда создается и его первая версия.
Чаще всего в качестве элементов для контроля версий выступают:
  • файлы;
  • директории;
  • hard- и softlinks.
Внутри системы контроля сами элементы могут размещаться по-разному – это зависит от архитекторы VCS. Пользователю важно лишь знать, что элемент помещается внутри хранилища и работа с ним идет с помощью команд выбранного инструментария.

Ветвление и слияние версий

Как уже было сказано, системы контроля должны предоставлять структуры для хранения версий. Самым распространенным представлением подобной структуры является дерево версий. Это такая организация версий элемента, при которой на основе любой версии элемента конфигурации может быть создано несколько наборов последовательностей его версий. При этом отдельный набор версий, происходящий из произвольной версии, называется веткой. И поскольку ветка содержит версии, то каждая из версий может быть источником для создания других веток. Короче, дерево.

Название модели говорит само за себя: у растений (элементов) появляются почки и листья (версии), из них, в свою очередь, ветки. На ветках – листья (другие версии) и другие ветки. На них, опять же, произрастает всё та же растительность. В результате растет дерево, у которого крона – это множество версий. Один элемент – одно дерево.

Зачем нужна вся эта конструкция? Неужели нельзя просто наращивать версии одну за другой? Конечно, можно. Однако это сразу ограничит возможности использования подобной системы. Если версии появляются одна за одной, то в один момент времени создать новую версию сможет только один из пользователей, работающих с системой, остальные вынуждены будут подождать. Более того, когда появится новая версия, каждому надо будет соединить свои изменения с текущими наработками. И так – пока все желающие не поместят свои наработки в цепочку версий. При этом каждый должен будет убедиться, что слияние версий не привело к поломке системы. И, кроме того, пока все изменения не будут помещены таким образом под контроль, всем из ожидающих придется сохранять промежуточные результаты где-то локально, не смешивая с тем, что находится в настоящий момент в работе. И ладно, если пара человек работает над десятком элементов – они всегда смогут договориться. А если масштабы гораздо больше? Добавим десяток человек (даже не увеличивая количества элементов) – и подобные простые цепочки полностью застопорят работу. В общем, линейная структура версий порождает множество сложностей.

Итак, ясно, что без веток не обойтись. Но ведь не растить же ветку на малейший чих разработчика? Посмотрим, в каких же случаях отращиваются ветки. Типовые примеры веток таковы:
  • ветка для запроса на изменения — заводится для версий, создаваемых в ходе работы по запросу на изменение («девелоперская», или «сиарная», ветка);
  • интеграционная ветка — служит промежуточным хранилищем для процесса стабилизации;
  • релизная ветка — для выкладывания версий при стабилизации конфигурации (см. соответствующий раздел первой части статьи). Какие-то версии на ветке могут быть в дальнейшем объявлены частью базовой конфигурации;
  • отладочная («дебажная») ветка — для кратковременного хранения версий, в основном, для целей проверки каких-либо решений.

Схема 1. Дерево версий элемента element.c

На схеме 1 изображен пример дерева версий. У файла element.c создана релизная ветка release_1.x, куда складываются стабилизированные версии этого элемента (1-5). Для сохранения дельты по каждому запросу на изменения заводится отдельная ветка со специальным форматом имени. В нашем случае формат имеет вид rec<номер_записи>_<имя_пользователя>, где номер_записи – это ID запроса на изменение в системе отслеживания. Для объединения дельты от разных разработчиков создаются интеграционные ветки с названиями вида int_<имя_пользователя>_<суффикс>, где суффикс хранит описание интеграции или номер стабилизируемой конфигурации. Также можно увидеть ветку для отладки, чаще всего они именуются как dbg_<имя пользователя>_<произвольный_комментарий> — на неё выкладываются проверочные варианты изменений.

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

Дерево версий растет и ширится, и рано или поздно надо результаты работы объединять. Например, разработчик отрастил ветку у одного из элементов для работы над запросом на изменение. На неё он положил несколько версий, и последняя является той, которая содержит отлаженный и протестированный код. В то же время есть релизная ветка, где лежат версии, выпускаемые в рамках базовых конфигураций и стабильных релизов. Необходимо соединить результаты.

Для этого используется механизм слияния версий. Как правило, он подразумевает создание новой версии элемента, для которой в качестве основы берется базовая версия на выбранной ветке (база), и к ней применяются изменения, содержащиеся в выбранной сторонней версии (источнике). В англоязычных источниках используется термин merge («мёрж»).

Ветка с версией-источником может быть отращена как от версии-источника, так и от его более ранних предков. Существующие VCS позволяют делать слияние как вручную, так и автоматически. Причем второй способ является основным. Ручное слияние запрашивается лишь в случае конфликтов.

Конфликты слияния возникают в случае, если в обоих версиях элемента меняется один и тот же фрагмент. Такая ситуация возникает когда предок версии-источника не является версией, от которой будет раститься новая версия. Типичным примером такого конфликта может служить история изменений (revision history), которая добавляется в начало файла исходников, чтобы в каждой версии можно было сразу видеть, кто последним менял и что было сделано. В случае слияния версий, отращенных от разных источников, эта строчка точно будет вызывать конфликт, и решается он лишь вставкой обеих строчек в историю. Когда возникает более сложный случай – разработчик или эксперт в затронутом коде должен внимательно вручную произвести нужные изменения.

К вопросу об общих предках и о слиянии изменений: кроме ручного и автоматического, слияние может быть произведено двухпозиционным и трёхпозиционным способом. Двухпозиционное слияние производится простым сравнением двух версий и сложением их дельты (разницы между версиями элемента). Алгоритм работает по принципу diff'а или приближенно к нему: взять дельту и вставить/удалить/изменить нужные строки.

Трехпозиционное слияние учитывает «общего предка» обеих версий и высчитывает дельту исходя из истории изменения элемента в соответствующих ветках. Соответственно, при возникновении конфликта слияния разработчику предлагается 3 версии элемента – общий предок и 2 варианта, что с этим предком стало с течением времени и изменений. Такой подход помогает оценить степень и важность дельты на обеих ветках и принять решение о необходимости интеграции конфликтного куска часто даже без участия авторов изменений.

После того как слияние проведено, информация о нём должна быть сохранена, если это возможно. Как правило, большинство зрелых VCS имеют возможность сохранить «стрелки слияния» – метаинформацию о том, откуда, куда и в каком момент времени сливались изменения и кто это делал.

Пример ветвления и слияния

Рассмотрим пример – дерево версий элемента на схеме 2, продемонстрировав порядок отращивания и слияния веток на нём. Как уже можно догадаться, дерево целиком взято со схемы 1, но к нему добавлены стрелки слияния.


Схема 2. Пример слияния изменений между разными ветками

Итак, в проекте производится некий продукт, в который входит файл element.c. Для того чтобы хранить стабилизированные версии, в команде принято соглашение о том, что все стабильные или базовые версии хранятся на ветке «release_1.x». Это будет называть «релизная ветка». Наш элемент не исключение, и на релизной ветке создается начальная версия 1.
Для простоты обозначения будем описывать ветки, как если бы это были директории на диске. Соответственно, первую версию назовем /release_1.x/1.

Далее, кто-то из менеджеров в системе отслеживания запросов на изменение (далее будем называть эту систему просто «багтрекер») завел запись номер 98, где описал новую функциональность, требуемую продукту. И, конечно же, назначил ответственным за эту задачу одного из пользователей – пусть это будет user2. user2 подумал немного и начал эту задачу решать, а по истечении какого-то времени решил выложить получившиеся исходники под контроль версий. Согласно стандартам именования, принятым в рамках проекта (CM-политикам), ветку для внесения изменений в нашем проекте называют rec<номер-записи>_<пользователь>[_<комментарии>]. Поэтому новая ветка была названа rec98_user2, а от комментариев её создатель воздержался. Работа кипит, появляется версия /release_1.x/rec98_user2/1, а потом и /release_1.x/rec98_user2/2.
На этом пока оставим разработчика user2, пусть думает над задачей. Ведь пока он работал, в багтрекере, была зарегистрирована запись (CR) за номером 121, в которой описали новую ошибку, найденную тестерами. Запись эта была назначена на пользователя user1, и он начал успешно описанную ошибку исправлять. По мере исправления он решил завести ветку для сохранения результатов. Новую ветку, согласно проектным политикам, пользователь назвал rec121_user1. Заметим, что на момент начала работы и создания ветки кто-то уже добавил очередную стабильную версию на релизную ветку – /release_1.x/2. Поэтому ветка отращивается от последней на тот момент версии (второй). Ветка создана – можно складывать версии. Конечный результат – версия /release_1.x/rec121_user1/2.

Что дальше? Ошибка исправлена, протестирована (эту плоскость работ мы пока оставим за кадром) – пора делать эти изменения частью стабильной конфигурации и, возможно, новой базовой конфигурацией. Здесь начинает работу CM-инженер или тот участник команды, который выполняет эту роль. С помощью лома и кувалды команды слияния он создает новую версию на релизной ветке — /release_1.x/3. Обратите внимание на стрелочку с номером 1 – она отображает как раз процесс слияния.

Вернемся к пользователю user2 – он как раз надумал сделать некоторые изменения для своей задачи, однако решил сначала на скорую руку проверить, что получится и дать коллегам посмотреть на своё решение. Для этого он создает отладочную ветку. CM-политика проекта говорит, что она должна называться dbg_<пользователь>[_<комментарий>]. Соответственно, новая ветка будет именоваться /release_1.x/rec98_user2/dbg_user2. На ней пользователь и создает версию /release_1.x/rec98_user2/dbg_user2/1. Было решено взять полученное решение в основной код, поэтому автор сделал слияние новой дельты и той версии, от которой отращивалась ветка. Вместе с тем, пользователь почистил и оптимизировал код, чтобы было не стыдно отдавать на интеграцию – в результате получилась версия /release_1.x/rec98_user2/3. Ну а яркая стрелочка под номером 2 наглядно обрисовывает процесс слияния.

Однако user2 узнает, что за время его работы была исправлена серьезная ошибка, на которую был заведен CR #121. И это исправление может повлиять на работу новой функциональности. Принимается решение соединить обе дельты и посмотреть, что из этого получится. Делается слияние версий /release_1.x/rec98_user2/3 и /release_1.x/rec121_user1/2 с образованием версии /release_1.x/rec98_user2/4. Ну и стрелочка слияния номер 3 также появляется. Эта новая версия проверяется на работоспособность и наличие ошибок, и принимается решение – надо интегрировать! Снова берет свои инструменты CM-инженер и делает версию /release_1.x/4, рисуя к ней соответствующую стрелку номер 4 (любое совпадение цифр – случайно).

Однако, жизнь не стоит на месте. Пока наши два разработчика вносили и сливали вместе дельту, другие участники команды уже изменили тот же самый файл. Было заведено два CR'а – 130 и 131, назначенных затем на пользователя user3. Он их успешно закончил и сделал две ветки – по одной на запись. Поскольку задачи ставились и решались в разное время, то и ветки для их решения отращивались от разных версий на релизной ветке. В итоге получились версии /release_1.x/rec130_user3/1 и /release_1.x/rec131_user3/1, отращенные от версии /release_1.x/3.

Есть изменения – надо их объединить, стабилизировать и сделать базовой конфигурацией, если всё нормально. Для этой цели CM-инженером, который в системе контроля версий проходит под оперативным псевдонимом user7, создается интеграционная ветка, имеющая в данном проекте вид int_<пользователь>_<номер-будущего-релиза>. Стало быть, появляется ветка /release_1.x/int_user7_1.5. На неё сливаются вместе обе дельты. Сначала изменения для записи 130, с образованием версии /release_1.x/int_user7_1.5/1. Затем – для записи 131, для неё создается версия 2 на той же ветке. Для всех операций рисуются стрелочки слияния.

Финальный аккорд CM-инженера – слияние версии /release_1.x/int_user7_1.5/2 на релизную ветку с образованием версии /release_1.x/5. Впоследствии эта ветка станет частью базовой конфигурации продукта.
Вот такое вот немаленькое описание маленькой картинки. Один рисунок стоит сотен слов – правду говорят.

У внимательного читателя в голове наверняка крутится вопрос – если у нас всё делается через ветки и стрелки слияния – откуда взялась версия /release_1.x/2? Ведь к ней не ведет ни одной стрелки ни от одной ветки! Закономерный вопрос. Ответ – тоже закономерен. Да, бывают ситуации, когда изменения вносятся напрямую на релизную ветку. Например, была найдена страшная ошибка, внесенная вместе с первой версией – забыли внести в раздел revision history комментарий о том, кто же внёс изменения! Конечно, это шутка, никто не будет нарушать политику ради вот таких вот мелочей. Однако – и такое случается. Главное – точно знать, кто создал новую версию и зачем он это сделал. Лучше всего, если система контроля версий позволяет ограничивать права на создание версий для каждой ветки в отдельности. В этом случае мы дополнительно обезопасим проект тем, что дадим права на добавление версий на релизной ветке только CM-инженеру. По крайней мере, с подобным ограничением будет проще найти крайнего :).

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

Метки

Итак, дерево версий растет, работа команды идет своим чередом. Возникает необходимость стабилизировать результаты работы и определить базовую конфигурацию, которую в любой момент времени любой участник команды сможет взять из системы контроля версий. Стабилизация производится путем слияния версий – об этом будет рассказано чуть ниже. А вот на установлении базиса остановимся подробнее.
Получение базовой конфигурации – это по сути своей выявление набора стабильных версий и определение способа их однозначной идентификации. Для этих целей в системах контроля версий существует механизм «навешивания меток». Метка – это цифро-буквенное обозначение, однозначно определяющее конфигурацию. Имея метку, нужно всегда уметь точно и недвусмысленно выделить конфигурацию.
В англоязычных источниках в основном используются термины label и tag.

Если метка – это обозначение для конфигурации, то и каждая из версий конфигурации должна однозначно определяться этой меткой. Таким образом, по метке будет выбираться элемент в той своей версии, которая помечена нужным образом.

Реализация концепции меток может различаться в разных системах. В одних (от CVS до ClearCase) метка – это атрибут версии элемента. Например, на схеме 2 метка бы вешалась прямо на одну из версий, т.е. была бы просто биркой рядом с кружком. В других системах (Subversion) под меткой понимается всего лишь одна из разновидностей веток. Каждому – своё, главное, чтобы вкладываемый смысл оставался тот же.

Что ещё хотелось бы отметить: одна конфигурация может быть помечена несколькими метками. Например, рассмотренные в первой части статьи компоненты могут метиться как компонентными метками (для определения базовых конфигураций компонентов), так и продуктовыми – для того чтобы официально становиться частью базовой конфигурации продукта. Получается, что базовая конфигурация каждой компоненты помечается, как минимум, два раза – один раз компонентной меткой, второй – продуктовой.
В целом, метки – это средство обозначения конфигураций, поэтому по большей части они являются инструментом работы CM-инженеров. Разработчики лишь используют уже сделанные метки для того, чтобы регулярно получать базис для дальнейшей работы.

Итог

Выше было показано, какми основными функциями обладает любая система контроля версий. Как они реализованы в конкретныо взятых приложениях — каждый может посмотреть в используемой лично им системе.
Взгляните на свои инструменти ещё раз, всежим взглядом и сравните с тем, что описано.

По традиции — список рекомендуемых материалов для самостоятельного вдумчивого чтения.
  1. en.wikipedia.org/wiki/Comparison_of_revision_control_software — большое сравнение существующих систем контроля версий
  2. www.cmcrossroads.com/bradapp/acme/branching/ — хорошая статья по политикам ведения веток, рассмотрено много разных шаблонов ветвления, подходящих для разных проектов.
  3. www.infoq.com/articles/agile-version-control — толковая статья о том, как можно организовать отращивание бранчей и их слияние при использовании agile-методик.

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

Продолжение следует.

P.S. На отдельной странице собраны ссылки на статьи по основам контроля версий и вообще управлению конфигурациями.

Комментариев нет:

Отправить комментарий